-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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.
| 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.
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.
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
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)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.
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.
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);/* 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));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'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));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 variable for the edge you compensate. Use
--container-padding-inline-start/--container-padding-inline-endfor horizontal bleed and--container-padding-block-start/--container-padding-block-endfor the first/last block edge. -
Always fall back to
0px. For example,var(--container-padding-inline-start, 0px)keeps the margin at zero outside a container. -
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 thecontainerPadding*VarStylesmaps). -
Don't use
--container-padding,--container-padding-inline, or--container-padding-block. Those variables were removed. Only the four per-edge variables exist.
<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.
<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.
<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
Start here Astryx Philosophy Contributing with AI Assistants Contributing
Architecture System Architecture Architecture Cheat Sheet Theming Infrastructure Distribution
Building a component Component Lifecycle Component Authoring Guide API Conventions Design Conventions
Quality Component Audit Rubric Accessibility Checklist
Operations Release Process Night Watch Overview