Skip to content

Container Padding System

Cindy Zhang edited this page Jun 23, 2026 · 1 revision

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 two CSS custom properties, who sets them, who reads them, and how to use them when building new components.


The Two Variables

Variable Direction Purpose
--container-padding-inline Horizontal (left/right) Inline bleed, edge compensation
--container-padding-block-start Top (block-start) Block bleed for :first-child
--container-padding-block-end Bottom (block-end) Block bleed for :last-child

All three default to 0px when unset. There is no isotropic --container-padding — all bleed is directional.

Why Three Variables?

Containers can have different inline and block padding. A Banner has padding-inline: 16px but padding-block: 12px. A TopNav has inline padding but no block padding. A Card with padding={0} has zero in both directions.

If a single isotropic variable tracked padding, a child reading it for block bleed would get the wrong value when inline and block differ — or get a stale default when the container explicitly sets padding to zero.

Block padding is split into start/end because Layout areas have asymmetric block padding. A Header has paddingBlockStart: outer-y but paddingBlockEnd: inner-y (the interior edge toward Content uses tighter spacing). A Footer is the reverse. A single --container-padding-block would give the wrong value for one edge. The split lets :first-child read block-start and :last-child read block-end independently.


Flow Diagram

Container (Card, Section, Layout area)
│
├── Sets --container-padding-inline      (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
    │   ├── marginInline: calc(-1 * var(--container-padding-inline, 0px))
    │   ├── width: calc(100% + 2 * var(--container-padding-inline, 0px))
    │   ├── 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: marginInline reads --container-padding-inline
    │   └── vertical: marginBlock reads --container-padding-block-start
    │
    ├── Section (nested)
    │   ├── marginInline reads --container-padding-inline
    │   ├── marginTop (:first-child) reads --container-padding-block-start
    │   └── marginBottom (:last-child) reads --container-padding-block-end
    │
    ├── Layout (outer wrapper)
    │   ├── marginInline reads --container-padding-inline
    │   ├── 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.)
        └── marginInline reads --container-padding-inline only

Setters

container() utility

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

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

// Card reads from --astryx-card-padding, Section from --astryx-section-padding
container({ useThemeDefault: 'card' })
// Sets:
//   --container-padding-inline:      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 with padding={N} prop):

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

Card and Section

When padding prop is set to a non-default value, Card and Section also apply containerPaddingInlineVarStyles and containerPaddingBlockVarStyles from padding.stylex.ts. These are keyed by spacing step (0, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 8, 10).

Layout areas (Header, Content, Footer, Panel)

Each Layout area component sets all three variables from their base styles, matching their 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 (no adjacent panel/header/footer). When an explicit padding prop is set, all three vars use that value, using the same containerPaddingInlineVarStyles and containerPaddingBlockVarStyles maps.

Direct setters

Some components set --container-padding-inline directly without container():

  • TopNav: --container-padding-inline: var(--spacing-2) — no block var needed since TopNav children don't bleed vertically
  • Banner: --container-padding-inline: var(--spacing-4) — same reason

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, 0px));
margin-inline-end:   calc(-1 * var(--container-padding-inline, 0px));
width: calc(100% + 2 * var(--container-padding-inline, 0px));

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

The table also uses --container-padding-inline 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, 12px), 8px);

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

Divider (isFullBleed)

/* Horizontal divider */
margin-inline: calc(-1 * var(--container-padding-inline, 0px));

/* Vertical divider */
margin-block: calc(-1 * var(--container-padding-block, 0px));

Section (nested)

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

margin-inline: calc(-1 * var(--container-padding-inline, 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 both variables for its own children:

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

Layout

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

margin-inline: calc(-1 * var(--container-padding-inline, 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: 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 directional variable for your axis. Use --container-padding-inline for horizontal bleed, --container-padding-block-start for :first-child top bleed, --container-padding-block-end for :last-child bottom bleed.

  2. Always fall back to 0px. var(--container-padding-inline, 0px) — if the component is used outside a container, the margin is 0 and nothing breaks.

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

    --container-padding-inline: 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 (no suffix) or --container-padding-block (no start/end). Those variables were removed. Only the three directional vars exist.


Common Patterns

Table in a zero-padding Card

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

All three vars (--container-padding-inline, --container-padding-block-start, --container-padding-block-end) 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 three vars are 16px (default). The table bleeds -16px on all sides, rows span edge-to-edge. First and last column cells get 16px inline padding to align content 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