On this page
Layout components
Primitive components for composing consistent spacing and layout structures without writing custom CSS.
- Overview
- Rules and guidance
- Properties
- Styles
- Accessibility
Layout components are low-level primitives that handle spacing and structural composition using theme tokens. They exist so that components and pages can use consistent spacing without writing one-off CSS.
When to use
- To add vertical spacing between a sequence of elements: use Stack
- To group short items that wrap across lines, such as buttons or badges: use Cluster
- To apply theme-token margin to any element without modifying its styles directly: use Margin
- To pull an element outside its parent's padding, for example a full-width image inside a padded card: use Bleed
- To create a responsive grid where columns are determined by a minimum item width, not a fixed column count: use ContentGrid
- To arrange two or more items side by side above a container-width threshold, falling back to stacked below: use Stack with horizontalAbove
When not to use
- For complex responsive page layouts - use Row and Col only as a last resort when no other layout component meets the need. Prefer Stack, Cluster, and ContentGrid.
- For one-off spacing adjustments - do not add custom margin or padding styles directly to components. Use Margin with a theme token instead.
- To make an element full-width on small screens - use the component's own fullWidth prop, for example on CTA Button, rather than writing custom responsive width styles. See Stretch for how this works internally.
Stack
Anatomy
- Container (the root div that owns the spacing contract)
- Items (direct children; spacing is applied between consecutive items)
Basic usage
Use spaceBetween with a theme token. The value can be a numeric scale key (1–12) or a semantic space token.
A numeric scale key:
Edit live code
<Stack spaceBetween={2}> <Block /* Example only */ /> <Block /* Example only */ /> <Block /* Example only */ /> </Stack>
A semantic space token, which keeps the spacing tied to its meaning rather than a fixed step:
Edit live code
<Stack spaceBetween="betweenCards"> <Block /* Example only */ /> <Block /* Example only */ /> <Block /* Example only */ /> </Stack>
Responsive spacing
Pass an array of space keys to change the gap at each breakpoint. The array maps to [default, smallAndUp, mediumAndUp, largeAndUp].
Edit live code
<Stack spaceBetween={[2, 4, 6, 8]}> <Block /* Example only */ /> <Block /* Example only */ /> <Block /* Example only */ /> </Stack>
Horizontal above a threshold
Pass a horizontalAbove container-width string to switch Stack from vertical to horizontal layout when the container is wide enough. This is container-based, not viewport-based, so it works correctly inside sidebars or narrow panels.
Drag the handle to resize the container and cross the 600px threshold:
Edit live code
<Resizable> <Stack spaceBetween={2} horizontalAbove="600px"> <Block /* Example only */ /> <Block /* Example only */ /> <Block /* Example only */ /> </Stack> </Resizable>
When horizontalAbove is set, items grow equally to fill available space. If you need unequal widths, use flexbox or a Grid directly.
ContentStack
ContentStack is a pre-configured Stack using the betweenCards spacing token. Use it for collections of cards or similar content blocks where you always want that token applied.
Edit live code
<ContentStack> <Card>Account one</Card> <Card>Account two</Card> <Card>Account three</Card> </ContentStack>
Pass horizontal to switch to a side-by-side layout above 750px.
Edit live code
<Resizable> <ContentStack horizontal> <Card>Account one</Card> <Card>Account two</Card> </ContentStack> </Resizable>
Cluster
Anatomy
- Container (a ul element using flexbox with wrapping)
- ClusterItem (an li element wrapping each child)
Cluster renders a ul/li structure. Wrap each child in a ClusterItem.
Basic usage
Edit live code
<Resizable> <Cluster spaceBetween={2}> <ClusterItem> <PrimaryCTAButton>Submit meter reading</PrimaryCTAButton> </ClusterItem> <ClusterItem> <SecondaryCTAButton>Cancel</SecondaryCTAButton> </ClusterItem> </Cluster> </Resizable>
Justification
Use the justify prop to control horizontal alignment. The default is flex-start.
Edit live code
<Resizable> <Cluster spaceBetween={2} justify="flex-end"> <ClusterItem> <Badge variant="success">Active</Badge> </ClusterItem> <ClusterItem> <Badge variant="warning">Non-communicating</Badge> </ClusterItem> <ClusterItem> <Badge>Peak</Badge> </ClusterItem> </Cluster> </Resizable>
Row gap
When items wrap to a second line, the vertical gap defaults to the same value as spaceBetween. Pass rowGap to use a different value.
Edit live code
<Resizable> <Cluster spaceBetween={8} rowGap={2}> <ClusterItem> <PrimaryCTAButton>Submit meter reading</PrimaryCTAButton> </ClusterItem> <ClusterItem> <SecondaryCTAButton>Cancel</SecondaryCTAButton> </ClusterItem> </Cluster> </Resizable>
Margin
Margin applies theme-token spacing as margin to any child element. Use it when you need to offset a single element without changing its own styles or creating a wrapper with custom CSS.
Anatomy
- Container (a div, or inline wrapper when inline is set, that applies margin)
Use top, bottom, left, or right to apply margin on a single side:
Edit live code
<> <Block /* Example only */ /> <Margin top={4}> <Block /* Example only */ /> </Margin> </>
Use horizontal, vertical, or all for shorthand.
Edit live code
<Margin all={4}> <Block /* Example only */ /> </Margin>
Bleed
Bleed applies negative margin to pull an element outside its parent's padding. Use it when you need something to appear full-width inside a padded container, such as a full-bleed image inside a Card or a divider that touches the card edges.
Anatomy
- Container (a div that uses negative margin to extend beyond its parent's padding)
The bled element extends past the Card's horizontal padding to touch its edges:
Edit live code
<Card> <P>This paragraph sits inside the card's padding.</P> <Bleed horizontal={[3, 4]}> <Block /* Example only */ /> </Bleed> </Card>
Pass inline to make the Bleed container display: inline-block rather than block.
Stretch
Stretch controls how wide an element is at different screen sizes. Use it when you need a button or other element to be full-width on small screens and auto-width on larger screens.
Note: Stretch is not exported from the Nebula public API. It is documented here because Nebula's own field and button components use it internally to implement their fullWidth prop. In application code, use the fullWidth prop on the component itself — for example CTA Button — rather than reaching for Stretch.
Anatomy
- Container (a div that sets width and max-width based on the fullWidth prop)
This example is not live, because Stretch is internal to Nebula and cannot be imported.
// Internal to Nebula - not importable from '@kaluza-platform/nebula'.
<Stretch fullWidth="small" defaultWidth="auto">
<PrimaryCTAButton>Continue</PrimaryCTAButton>
</Stretch>
fullWidth values:
- always: always 100% wide
- never: always defaultWidth
- small: 100% on small screens, defaultWidth on smallAndUp
ContentGrid
ContentGrid uses CSS Grid with auto-fit and minmax to fit as many columns as possible at a minimum item width, without fixed column counts or media queries.
Anatomy
- Container (a div using CSS Grid)
- Items (direct children placed into auto-sized columns)
Edit live code
<ContentGrid min="280px" spaceBetween={4}> <Card>Account one</Card> <Card>Account two</Card> <Card>Account three</Card> </ContentGrid>
Orphaned items on the last row expand to fill available space rather than leaving gaps. Pass rowGap to use a different vertical spacing value to spaceBetween.
Row and Col
Row and Col form a 12-column grid system. Use them only as a last resort when Stack, Cluster, and ContentGrid cannot meet the layout need.
Anatomy
- Row (the grid container, centred with a max-width set to breakpoints.large)
- Col (a column cell; defaults to full width, span 12)
Edit live code
<Row> <Col span={6}>Left content</Col> <Col span={6}>Right content</Col> </Row>
Use the small, medium, and large props on Col to change the column span at each breakpoint.
Edit live code
<Row> <Col span={12} small={6} large={4}>Sidebar</Col> <Col span={12} small={6} large={8}>Main content</Col> </Row>
Nest rows by passing isNested to the inner Row.
usage
- All layout components must use a SpaceKey token (numeric scale 1-12 or semantic name) for all spacing props, never a raw pixel or rem valueRaw values bypass the design system spacing scale and break cross-component consistency.Copy rule ID for layout-components.use-theme-tokens-for-spacing
- Consuming code must not apply custom margin or padding directly to Nebula components via inline styles or CSS overridesUse Margin or Stack to add spacing. Applying it directly creates specificity conflicts and makes spacing invisible to reviewers.Copy rule ID for layout-components.no-custom-margin-on-components
- Stack must receive a spaceBetween propThere is no default; omitting it produces no spacing.Copy rule ID for layout-components.space-between-required
- Stack horizontalAbove must receive a container-width string value, not a viewport-width media query stringStack uses container width via the intrinsic sizing trick. A viewport value will not work as expected.Copy rule ID for layout-components.horizontal-above-uses-container-width
- Stack spaceBetween must order responsive array values from smallest breakpoint to largest ([default, smallAndUp, mediumAndUp, largeAndUp])Copy rule ID for layout-components.responsive-array-order
- Stretch must receive a defaultWidth value when fullWidth is set to never or smallWithout defaultWidth the element falls back to auto, which may not match the intended size.Copy rule ID for layout-components.default-width-required-when-not-always
- ContentGrid must receive both min and spaceBetween propsCopy rule ID for layout-components.min-and-space-between-required
- Consuming code should use Stack or Cluster before writing a custom flexbox wrapper for spacingPrimitives enforce token use and communicate intent. Raw flexbox wrappers can use arbitrary pixel values.Copy rule ID for layout-components.prefer-primitives-over-raw-flexbox
- Stack should use a semantic space token (betweenFormFields, betweenCards) rather than a numeric key when the content matches a named patternSemantic tokens can be updated globally without touching individual usage sites.Copy rule ID for layout-components.use-semantic-tokens-for-known-patterns
- ContentStack should be used for collections of Card components or similarly sized content blocksContentStack is pre-configured with betweenCards spacing. Using raw Stack with betweenCards achieves the same result but is less expressive.Copy rule ID for layout-components.for-card-collections
- Cluster should be used for groups of items that may wrap to multiple lines and require consistent gap spacing in both axesCopy rule ID for layout-components.for-wrapping-inline-groups
- Cluster should use rowGap when the vertical gap between wrapped rows should differ from spaceBetweenCopy rule ID for layout-components.row-gap-for-wrap
- Margin should be used to add token-based spacing around a single element that does not participate in a Stack or Cluster layoutCopy rule ID for layout-components.use-for-single-element-offset
- Bleed spacing value should match the spacing token used for the parent container's paddingMismatched values leave a visible gap or overlap.Copy rule ID for layout-components.match-parent-padding
- Stretch should be used when an element needs to be full-width on small screens and a fixed width on larger screens, rather than writing custom responsive CSSCopy rule ID for layout-components.use-for-responsive-width
- Col should have span values across sibling columns that sum to 12 at each breakpointSpans that sum to more than 12 cause overflow; less than 12 leave unintended blank space.Copy rule ID for layout-components.spans-sum-to-twelve
- ContentStack horizontal may be used to switch to a side-by-side layout at 750pxThe threshold is fixed at 750px. If a different threshold is needed, use Stack with horizontalAbove directly.Copy rule ID for layout-components.horizontal-at-750px
- Cluster may omit justify when left-aligned behaviour is needed, as flex-start is the defaultCopy rule ID for layout-components.justify-default
- Margin should not be used as a substitute for Stack or Cluster when spacing multiple siblingsMargin on individual items is harder to read and maintain than a shared spacing contract on a parent.Copy rule ID for layout-components.not-as-layout-substitute
- ContentGrid should not be used when a fixed column count is required regardless of container widthContentGrid uses auto-fit. If a design requires exactly two or three columns at all sizes, use Row/Col or a custom grid.Copy rule ID for layout-components.not-for-fixed-column-counts
- Row / Col should not be used when Stack, Cluster, or ContentGrid can achieve the same layoutRow/Col is a legacy 12-column system. The other primitives are more expressive and easier to reason about.Copy rule ID for layout-components.last-resort-only
placement
- Bleed should be used only inside a padded container, where the bleed amount matches the container's padding tokenBleed uses negative margin. Applying it outside a padded container produces unexpected layout shifts.Copy rule ID for layout-components.only-inside-padded-container
composition
- Cluster must wrap each direct child in a ClusterItem elementCluster renders a ul/li structure. Children placed directly inside Cluster without a ClusterItem produce invalid HTML.Copy rule ID for layout-components.wrap-children-in-cluster-item
- Col must be a direct child of RowCol uses negative-margin-compensating padding that only works correctly inside a Row.Copy rule ID for layout-components.col-inside-row
- Row must set isNested to true when placed inside a ColNested rows need adjusted negative margins to compensate for the parent Col's padding.Copy rule ID for layout-components.nested-row-uses-is-nested
Stack
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
spaceBetween | SpaceValue | — | Yes | Gap between children, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). |
horizontalAbove | string | — | No | CSS width (for example `750px`) of the stack's own container above which children sit side by side in a row. Below it they stack vertically. Without it the stack is always vertical. |
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
ContentStack
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
horizontal | boolean | — | No | Lay children out side by side when the container is wider than `750px`, stacking them below that. Spacing is always `betweenCards`. |
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
Margin
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
bottom | SpaceValue | — | No | Margin below the content, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). |
top | SpaceValue | — | No | Margin above the content, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). |
left | SpaceValue | — | No | Margin to the left of the content, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). |
right | SpaceValue | — | No | Margin to the right of the content, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). |
inline | boolean | — | No | Renders as `display: inline-block` instead of a block-level element. |
horizontal | SpaceValue | — | No | Margin on both the left and right, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). |
vertical | SpaceValue | — | No | Margin on both the top and bottom, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). |
all | SpaceValue | — | No | Margin on all four sides, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). Takes precedence over the other margin props. |
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
Bleed
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
bottom | SpaceValue | — | No | Negative margin below the content, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). |
top | SpaceValue | — | No | Negative margin above the content, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). |
left | SpaceValue | — | No | Negative margin to the left of the content, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). |
right | SpaceValue | — | No | Negative margin to the right of the content, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). |
inline | boolean | — | No | Renders as `display: inline-block` instead of a block-level element. |
horizontal | SpaceValue | — | No | Negative margin on both the left and right, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). |
vertical | SpaceValue | — | No | Negative margin on both the top and bottom, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). |
all | SpaceValue | — | No | Negative margin on all four sides, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). Takes precedence over the other margin props. |
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
Cluster
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
justify | CSSProperties['justifyContent'] | 'flex-start' | No | Horizontal alignment of items within each row, as a CSS `justify-content` value. |
spaceBetween | SpaceValue | — | Yes | Gap between items, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). Applies to both rows and columns unless `rowGap` is set. |
rowGap | SpaceValue | — | No | Gap between wrapped rows, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). Overrides the row part of `spaceBetween`. |
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
ClusterItem
Also accepts all props from HTMLAttributes, LiHTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
ContentGrid
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
min | string | — | Yes | Minimum width of each column as a CSS length (for example `20rem`). As many equal columns as fit are created, and a column never exceeds the container's width. |
spaceBetween | SpaceValue | — | Yes | Gap between grid items, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). Applies to both rows and columns unless `rowGap` is set. |
rowGap | SpaceValue | — | No | Gap between rows, as a spacing key (`1`-`12` or a semantic key such as `betweenCards`) or an array of keys for responsive values (base, then `small`, `medium`, `large` breakpoints and up). Overrides the row part of `spaceBetween`. |
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
Row
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
isNested | boolean | false | No | Set when the row sits inside a `Col`. Removes the fixed width and applies negative side margins to offset the parent column's padding. |
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
Col
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
span | number | 12 | No | Number of columns (out of 12) the column spans below the `small` breakpoint, and at larger sizes unless `small`, `medium` or `large` override it. |
large | number | — | No | Number of columns (out of 12) to span from the `large` breakpoint up. Falls back to the next smaller value that is set. |
small | number | — | No | Number of columns (out of 12) to span from the `small` breakpoint up. Falls back to `span` if not set. |
medium | number | — | No | Number of columns (out of 12) to span from the `medium` breakpoint up. Falls back to the next smaller value that is set. |
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
Styles
stack
Defined in Stack/Stack.tsx
&[horizontalAbove] {
display: flex;
flex-wrap: wrap;
${useResponsiveStyle('gap', useSpaceValue(spaceBetween))}
> * {
flex-grow: 1;
flex-basis: calc((${horizontalAbove} - 100%) * 999);
}
}
@when !horizontalAbove {
> * + * {
${useResponsiveStyle('margin-top', useSpaceValue(spaceBetween))}
}
}
margin
Defined in Margin/Margin.tsx
&[horizontal] {
${useResponsiveStyle('margin-left', useSpaceValue(horizontal))}
${useResponsiveStyle('margin-right', useSpaceValue(horizontal))}
}
&[vertical] {
${useResponsiveStyle('margin-top', useSpaceValue(vertical))}
${useResponsiveStyle('margin-bottom', useSpaceValue(vertical))}
}
&[top] {
${useResponsiveStyle('margin-top', useSpaceValue(top))}
}
&[bottom] {
${useResponsiveStyle('margin-bottom', useSpaceValue(bottom))}
}
&[left] {
${useResponsiveStyle('margin-left', useSpaceValue(left))}
}
&[right] {
${useResponsiveStyle('margin-right', useSpaceValue(right))}
}
&[all] {
${useResponsiveStyle('margin', useSpaceValue(all))}
}
&[inline] {
display: inline-block;
}
bleed
Defined in Bleed/Bleed.tsx
&[horizontal] {
${useResponsiveStyle( 'margin-left', useSpaceValue(horizontal, formatNegative), )}
${useResponsiveStyle( 'margin-right', useSpaceValue(horizontal, formatNegative), )}
}
&[vertical] {
${useResponsiveStyle( 'margin-top', useSpaceValue(vertical, formatNegative), )}
${useResponsiveStyle( 'margin-bottom', useSpaceValue(vertical, formatNegative), )}
}
&[top] {
${useResponsiveStyle('margin-top', useSpaceValue(top, formatNegative))}
}
&[bottom] {
${useResponsiveStyle( 'margin-bottom', useSpaceValue(bottom, formatNegative), )}
}
&[left] {
${useResponsiveStyle('margin-left', useSpaceValue(left, formatNegative))}
}
&[right] {
${useResponsiveStyle('margin-right', useSpaceValue(right, formatNegative))}
}
&[all] {
${useResponsiveStyle('margin', useSpaceValue(all, formatNegative))}
}
&[inline] {
display: inline-block;
}
cluster
Defined in Cluster/Cluster.tsx
display: flex;
flex-wrap: wrap;
justify-content: ${justify};
align-items: center;
${useResponsiveStyle('gap', useSpaceValue(spaceBetween))}
${useResponsiveStyle('row-gap', useSpaceValue(rowGap))}
col
Defined in Grid/Col.tsx
flex: 0 0 auto;
flex-basis: ${(span / 12) * 100}%;
max-width: ${(span / 12) * 100}%;
box-sizing: border-box;
padding-left: ${halfGutter[0]};
padding-right: ${halfGutter[0]};
margin-bottom: ${space[4]};
width: 100%;
${mediaQueries.smallAndUp} {
padding-left: ${halfGutter[1]};
padding-right: ${halfGutter[1]};
margin-bottom: ${space[6]};
@when smallPercentage {
flex-basis: ${small ? (small / 12) * 100 : null}%;
max-width: ${small ? (small / 12) * 100 : null}%;
}
}
@when mediumPercentage {
${mediaQueries.mediumAndUp} {
flex-basis: ${medium ? (medium / 12) * 100 : null}%;
max-width: ${medium ? (medium / 12) * 100 : null}%;
}
}
@when largePercentage {
${mediaQueries.largeAndUp} {
flex-basis: ${large ? (large / 12) * 100 : null}%;
max-width: ${large ? (large / 12) * 100 : null}%;
}
}
content-grid
Defined in Grid/ContentGrid.tsx
display: grid;
${useResponsiveStyle('gap', useSpaceValue(spaceBetween))}
${useResponsiveStyle('row-gap', useSpaceValue(rowGap))}
@supports (width: min(${min}, 100%)) {
grid-template-columns: repeat(auto-fit, minmax(min(${min}, 100%), 1fr));
}
row
Defined in Grid/Row.tsx
display: flex;
box-sizing: border-box;
flex: 0 1 auto;
flex-direction: row;
flex-wrap: wrap;
padding-left: ${halfGutter[0]};
padding-right: ${halfGutter[0]};
margin: 0 auto;
width: ${breakpoints.large};
max-width: 100%;
margin-bottom: -${space[4]};
& + & {
margin-top: ${space[4]};
}
&[isNested] {
margin-left: -${space[4]};
margin-right: -${space[4]};
width: auto;
max-width: none;
}
${mediaQueries.smallAndUp} {
padding-left: ${halfGutter[1]};
padding-right: ${halfGutter[1]};
margin-bottom: -${space[8]};
& + & {
margin-top: ${space[8]};
}
&[isNested] {
margin-left: -${space[8]};
margin-right: -${space[8]};
}
}
In brief
The layout components shape the page and say nothing about it. Cluster is a list, and every other one is a plain div, so a screen reader announces the content and none of the layout.
What Nebula provides
- None of the layout components change the order of their children, so the reading order is the order in the code.
- Stack, ContentGrid and Row wrap their children when the container is narrow, and ContentGrid never makes a column wider than its container.
- Cluster renders a ul and ClusterItem an li.
Accessible design
Keep the visual order and the reading order the same
The components lay children out in the order of the code, and a keyboard or screen reader user moves through the page in that order. Design each layout so that reading it left to right and top to bottom, at every width, gives the same order.
WCAG: 1.3.2 Meaningful Sequence 2.4.3 Focus Order
Check each layout at 320 pixels wide
A layout that holds only at a desktop width forces a low-vision user who zooms in to scroll sideways as well as down. Stack and ContentStack switch to a row by the width of their own container, not the screen.
WCAG: 1.4.10 Reflow
Accessible development
Always add the landmark elements yourself
The layout components are div elements and add no landmarks, so a page built only from them has none. Put main, nav and the other landmark elements around them, keep to one main per page and label a landmark that appears more than once.
WCAG: 1.3.1 Info and Relationships 2.4.1 Bypass Blocks
Pass only ClusterItem as children of Cluster
Cluster is a ul, and a list only holds li elements. Anything else inside it breaks the list for a screen reader.
WCAG: 1.3.1 Info and Relationships
Don't reorder children with CSS
Setting order or a reverse flex direction on a child moves it on screen but not in the reading order or the tab order.
WCAG: 1.3.2 Meaningful Sequence 2.4.3 Focus Order
Accessibility testing status
Reviewed on 9 October 2026 against v17.0.1.
| Test | Status | Tested with |
|---|---|---|
| Default state | Tested | axe-core via the Storybook test runner (CI) |
| Advanced states | Partially tested | axe-core via the Storybook test runner (CI) |
| Keyboard navigation | Manually tested | keyboard (reported by the owner) |
| Screen reader | Partially tested | VoiceOver with Chrome (reported by the owner) |
What these tests and statuses mean
The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used.
The tests
- Default state: The component as it first appears, before anything is clicked or changed.
- Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering.
- Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring.
- Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state.
The statuses
- Tested: Every check in this row passed in an automated run that repeats on every pull request.
- Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change.
- Partially tested: Some checks in this row were run once and passed; the rest are still to do.
- Issue found: A check in this row failed. The ticket is shown beside the status.
- Not tested: No check in this row has been run yet.
- Not applicable: This row does not apply to the component, for the reason given beside it.