Skip to content

Container Padding System

Cindy Zhang edited this page Aug 26, 2026 · 2 revisions

Container Padding System

Astryx containers (Card, Section, Layout areas) communicate their padding to children via CSS custom properties. Children like Table, Divider, and Section use these vars to "bleed" — escape the container's padding so they span edge-to-edge.

This page documents the four directional CSS custom properties, who sets them, who reads them, and how to use them when building new components.


The Four Variables

Variable Direction Purpose
--container-padding-inline-start Inline start (left in LTR, right in RTL) Start-edge bleed and cell compensation
--container-padding-inline-end Inline end (right in LTR, left in RTL) End-edge bleed and cell compensation
--container-padding-block-start Block start (top in horizontal writing) Start-edge bleed for :first-child
--container-padding-block-end Block end (bottom in horizontal writing) End-edge bleed for :last-child

All four use a 0px fallback when read outside a container. There is no isotropic --container-padding, --container-padding-inline, or --container-padding-block — every bleed reads the edge it compensates.

Why Four Variables?

Containers can paint different padding on every logical edge. Layout headers and footers use different block-start and block-end values, and Section can override inline-start independently of inline-end. A single axis variable would make one edge compensate against the wrong value.

The split also preserves RTL behavior: inline-start and inline-end follow writing direction without turning the protocol into physical left/right variables.


Flow Diagram

Container (Card, Section, Dialog, Layout area)
│
├── Sets --container-padding-inline-start (from paddingOuterX or theme default)
├── Sets --container-padding-inline-end   (from paddingOuterX or theme default)
├── Sets --container-padding-block-start  (from paddingOuterY or theme default)
├── Sets --container-padding-block-end    (from paddingOuterY or theme default)
│
└── Child components read them:
    │
    ├── Table
    │   ├── marginInlineStart reads --container-padding-inline-start
    │   ├── marginInlineEnd reads --container-padding-inline-end
    │   ├── width adds both inline edge values
    │   ├── marginTop (:first-child): calc(-1 * var(--container-padding-block-start, 0px))
    │   └── marginBottom (:last-child): calc(-1 * var(--container-padding-block-end, 0px))
    │
    ├── Divider (isFullBleed)
    │   ├── horizontal: each inline margin reads its matching edge
    │   └── vertical: block margins read their matching start/end edges
    │
    ├── Section (nested)
    │   ├── inline margins read their matching start/end edges
    │   ├── marginTop (:first-child) reads --container-padding-block-start
    │   └── marginBottom (:last-child) reads --container-padding-block-end
    │
    ├── Layout (outer wrapper)
    │   ├── inline margins read their matching start/end edges
    │   ├── marginBlockStart reads --container-padding-block-start
    │   ├── marginBlockEnd reads --container-padding-block-end
    │   └── fill height compensates with block-start + block-end
    │
    └── Edge compensation (ghost buttons, etc.)
        └── inline compensation follows the applicable start/end edge

Setters

container() utility

The container() function in Layout/container.stylex.ts is the central setter. It returns a StyleX style array that sets all four variables.

Theme-default path (Card/Section/Dialog with no explicit padding prop):

// Card reads from --astryx-card-padding, Section from --astryx-section-padding
container({ useThemeDefault: 'card' })
// Sets:
//   --container-padding-inline-start: var(--astryx-card-padding, 16px)
//   --container-padding-inline-end:   var(--astryx-card-padding, 16px)
//   --container-padding-block-start:  var(--astryx-card-padding, 16px)
//   --container-padding-block-end:    var(--astryx-card-padding, 16px)

Explicit padding path (Card/Section/Dialog with an explicit padding prop):

container({ paddingOuterX: 'spacing2', paddingOuterY: 'spacing0' })
// Sets:
//   --container-padding-inline-start: 8px (from paddingOuterX)
//   --container-padding-inline-end:   8px (from paddingOuterX)
//   --container-padding-block-start:  0px (from paddingOuterY)
//   --container-padding-block-end:    0px (from paddingOuterY)

Card, Section, and Dialog

When an explicit padding prop is set, container owners use the maps in padding.stylex.ts. The combined inline map writes both inline edges; per-edge maps let Section publish asymmetric inline padding. Block start and block end always have separate maps.

Layout areas (Header, Content, Footer, Panel)

Each Layout area component sets all four variables from its base styles, matching its actual CSS padding per edge. Header publishes block-start: outer-y, block-end: inner-y; Footer is the reverse. Content uses inner-x/inner-y by default, upgrading to outer-x/outer-y at container edges. An explicit padding prop updates all four through the shared maps.

Consumers (Bleed Components)

Table

The table's containerBleed style makes it span edge-to-edge inside any container:

/* Inline bleed — always applied */
margin-inline-start: calc(-1 * var(--container-padding-inline-start, 0px));
margin-inline-end:   calc(-1 * var(--container-padding-inline-end, 0px));
width: calc(100% + var(--container-padding-inline-start, 0px)
                  + var(--container-padding-inline-end, 0px));

/* Block bleed — only first/last child */
margin-top (:first-child):    calc(-1 * var(--container-padding-block-start, 0px));
margin-bottom (:last-child):  calc(-1 * var(--container-padding-block-end, 0px));

The table also uses the matching inline edge for cell edge compensation — first and last column cells get extra padding to align content with the container's content inset:

/* First column cell */
padding-inline-start: max(var(--container-padding-inline-start, 12px), 8px);

/* Last column cell */
padding-inline-end: max(var(--container-padding-inline-end, 12px), 8px);

Divider (isFullBleed)

/* Horizontal divider */
margin-inline-start: calc(-1 * var(--container-padding-inline-start, 0px));
margin-inline-end: calc(-1 * var(--container-padding-inline-end, 0px));
width: calc(100% + var(--container-padding-inline-start, 0px)
                 + var(--container-padding-inline-end, 0px));

/* Vertical divider */
margin-block-start: calc(-1 * var(--container-padding-block-start, 0px));
margin-block-end: calc(-1 * var(--container-padding-block-end, 0px));
height: calc(100% + var(--container-padding-block-start, 0px)
                  + var(--container-padding-block-end, 0px));

Section (nested)

When a Section is nested inside another container, it escapes the parent's padding:

margin-inline-start: calc(-1 * var(--container-padding-inline-start, 0px));
margin-inline-end: calc(-1 * var(--container-padding-inline-end, 0px));
margin-top (:first-child): calc(-1 * var(--container-padding-block-start, 0px));
margin-bottom (:last-child): calc(-1 * var(--container-padding-block-end, 0px));

The Section's inner wrapper then resets all four variables for its own children:

--container-padding-inline-start: 0px;
--container-padding-inline-end: 0px;
--container-padding-block-start: 0px;
--container-padding-block-end: 0px;

Layout

Layout's outer wrapper escapes the container in both directions:

margin-inline-start: calc(-1 * var(--container-padding-inline-start, 0px));
margin-inline-end: calc(-1 * var(--container-padding-inline-end, 0px));
margin-block-start: calc(-1 * var(--container-padding-block-start, 0px));
margin-block-end: calc(-1 * var(--container-padding-block-end, 0px));

The inner wrapper resets for descendants:

--container-padding-inline-start: 0px;
--container-padding-inline-end: 0px;
--container-padding-block-start: 0px;
--container-padding-block-end: 0px;

In fill height mode, the Layout compensates for its own negative block margins:

height: calc(100% + var(--container-padding-block-start, 0px) + var(--container-padding-block-end, 0px));

Edge compensation

Ghost buttons, tabs, and other transparent-padding components create excess visual space at container edges (the container's padding + the component's own transparent padding doubles up). Edge compensation is container-driven — containers detect and adjust, components just declare eligibility.

How it works:

  1. Components render data-astryx-edge-comp="" to declare they're edge-compensatable. Ghost buttons and tabs do this automatically.
  2. Containers (Toolbar, Banner, etc.) apply edgeCompSlot.inset(amount) on their slot wrappers, which uses :has(> [data-astryx-edge-comp]:first-child) / :last-child) selectors to pull slot margins at edges.

The container owns both detection and adjustment. Components are passive.

// Component side — just a data attribute, no styles:
<button data-astryx-edge-comp="" {...props}>...</button>

// Container side — slot wrapper pulls margin when edge child is compensatable:
import {edgeCompSlot} from '../Layout/edgeCompensation.stylex';

<div {...stylex.props(styles.startSlot, edgeCompSlot.inset(spacingVars['--spacing-2']))}>
  {startContent}
</div>

Wrapper components (like TabList) that contain compensatable items should also render data-astryx-edge-comp on their root element, since the :has(>) direct-child selector won't reach nested descendants.

See edgeCompensation.stylex.ts for the implementation.


Adding Bleed to a New Component

If you're building a component that needs to escape container padding:

  1. Read the variable for the edge you compensate. Use --container-padding-inline-start / --container-padding-inline-end for horizontal bleed and --container-padding-block-start / --container-padding-block-end for the first/last block edge.

  2. Always fall back to 0px. For example, var(--container-padding-inline-start, 0px) keeps the margin at zero outside a container.

  3. If your component creates a new container context, reset all four variables for descendants:

    --container-padding-inline-start: 0px;
    --container-padding-inline-end: 0px;
    --container-padding-block-start: 0px;
    --container-padding-block-end: 0px;

    Then set them to your component's actual padding values (via container() utility or the containerPadding*VarStyles maps).

  4. Don't use --container-padding, --container-padding-inline, or --container-padding-block. Those variables were removed. Only the four per-edge variables exist.


Common Patterns

Table in a zero-padding Card

<Card padding={0}>
  <Table data={data} columns={columns} />
</Card>

All four vars are 0px. The table applies zero negative margins — it just sits flush. No overflow.

Table in a standard Card

<Card>
  <Table data={data} columns={columns} />
</Card>

All four vars are 16px (default). The table bleeds -16px on all sides, rows span edge-to-edge. First and last column cells read their matching inline edge to align with the Card's content area.

Table with header above it in a Card

<Card>
  <VStack gap={3}>
    <h2>Users</h2>
    <Table data={data} columns={columns} />
  </VStack>
</Card>

The table is not :first-child (the heading is), so marginTop stays at default: null. Only marginBottom applies if the table is :last-child. Inline bleed still works — the table still spans edge-to-edge horizontally.


Related Pages

Clone this wiki locally