-
Notifications
You must be signed in to change notification settings - Fork 27
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.
| 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.
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.
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
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)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).
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.
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
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);/* Horizontal divider */
margin-inline: calc(-1 * var(--container-padding-inline, 0px));
/* Vertical divider */
margin-block: calc(-1 * var(--container-padding-block, 0px));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'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));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:
-
Components render
data-astryx-edge-comp=""to declare they're edge-compensatable. Ghost buttons and tabs do this automatically. -
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.
If you're building a component that needs to escape container padding:
-
Read the directional variable for your axis. Use
--container-padding-inlinefor horizontal bleed,--container-padding-block-startfor:first-childtop bleed,--container-padding-block-endfor:last-childbottom bleed. -
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. -
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 thecontainerPadding*VarStylesmaps). -
Don't use
--container-padding(no suffix) or--container-padding-block(no start/end). Those variables were removed. Only the three directional vars exist.
<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.
<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.
<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.
- System Architecture — Overall Astryx architecture and customization layers
-
Theming Infrastructure — How themes override padding via
--astryx-card-padding/--astryx-section-padding - API Conventions — Component API design principles
- Component Authoring Guide — How to build new components with StyleX and tokens