Container
Max-width wrapper with page gutters. Seven widths on the field's common scale, a responsive gutter ladder, a breakout grid for full-bleed children, published CSS variables for custom widths and edge-to-edge rails, and polymorphic rendering.
Playground
Installation
pnpm add @tessinaui/uiUsage
import { Container } from "@tessinaui/ui";{/* The default: a 1152px cap, responsive gutters, centred */}
<Container>…</Container>
{/* A reading column, rendered as the page's <article> */}
<Container size="sm" render={<article />}>…</Container>
{/* Fluid width, gutters only — a dashboard */}
<Container size="full">…</Container>
{/* One container for a flowing page; one child breaks out to full width */}
<Container layout="breakout" size="sm">
<p>In the column.</p>
<figure data-breakout>…edge to edge…</figure>
<p>Back in the column.</p>
</Container>
{/* A width off the scale */}
<Container style={{ "--container-size": "40rem" } as React.CSSProperties}>…</Container>Examples
Default
Content capped at lg (1152px), with gutters that step with the viewport, centred in its parent.
Sizes
Seven widths, plus a custom one set through the published variable. A size is a cap: when the parent is narrower — as this preview is for the larger sizes — the Container renders at the parent's width, which the readout shows live.
Padding
The gutter inside the box. responsive, the default, steps 16 → 24 → 32 → 48px at the md, lg and xl breakpoints.
Alignment
center={false} holds the box to the start edge. Start is logical, so under dir="rtl" the same prop lands it on the right.
Reading column
sm is the reading column. With the widest gutter its content box is 576px — about 75 characters of 16px body text per line, the top of the 45–75 range typographers recommend — so the eye's return sweep stays short. Here it renders as the page's <article>.
Form column
A settings or profile form reads best in a narrow column — long inputs are harder to scan and invite over-long answers. xs (448px) is the form width on this scale.
Fluid dashboard
size="full" removes the cap and keeps the gutters, which is the shape of most dashboards. Let rows reflow by their own width (auto-fit) rather than the viewport's, so they behave inside any pane.
Nested widths
Two widths on one page: a narrow inner Container keeps a headline short while the row below uses the outer width. Give the inner one padding="none" so the gutter is not counted twice.
Full-bleed band
A painted section wrapping a Container: the background runs edge to edge while the copy stays readable. The section is the band — no outer size="full" Container is needed.
Breakout
layout="breakout" turns one Container into a three-track grid for flowing content. Every child lands in the centre column; a child marked data-breakout spans the full width. A band whose own content should line up with the column nests a Container of the same size and padding.
Edge-to-edge rail
The published --container-px lets a child escape the gutter. A horizontal rail runs to the Container's edge — the screen edge on a phone — while its first card still lines up with the text.
Semantic HTML
render keeps Container's styling on a semantic element.
Sizes
| Size | Cap | Use | Where the field lands |
|---|---|---|---|
xs | 448px (28rem) | Forms, settings, modal-width content | Radix size-1 448 · MUI xs 444 |
sm | 672px (42rem) | Reading column | Apple's readable content guide 672pt · Radix 688 · Polaris narrow 662 |
md | 896px (56rem) | Long-form article | Atlassian fixed-narrow 864 · Radix 880 |
lg | 1152px (72rem) | Default — app content, pricing | Radix 1136 · Bootstrap and Mantine 1140 · MUI lg 1200 |
xl | 1280px (80rem) | Wide pages | Primer xl 1280 · Spectrum 1280 |
2xl | 1536px (96rem) | Ultra-wide dashboards, data tables | MUI xl 1536 |
full | 100% | Fluid — gutters only | Every system's "fluid" |
The cap is the box's outer width, gutters included: at lg with 48px gutters the content is 1056px wide.
Padding
| Padding | Gutter per side | Use |
|---|---|---|
none | 0 | The parent already provides the gutter |
xs | 8px | Dense layouts |
sm | 16px | Phones — Material's compact margin and the common iOS page margin |
md | 24px | Standard |
lg | 32px | Generous |
xl | 48px | Marketing and wide desktop pages |
responsive | 16 → 24 → 32 → 48px | Default — steps at md / lg / xl |
responsive is written as four disjoint bands — max-md: · md:max-lg: · lg:max-xl: · xl: — never as one base value with min-width overrides. In Tailwind v4 the winner of a min-width ladder is decided by stylesheet emission order, and the old one rendered 24px at 1400px where it asked for 48.
The bands step with the viewport. Inside a narrow pane on a wide screen, pick a fixed padding instead.
Layout: block and breakout
block (the default) is a max-width box with inline padding, centred with mx-auto.
breakout is a grid of three tracks — 1fr · column · 1fr. The column is exactly as wide as a block Container's content box, so the two layouts line up on one page, and the side tracks are never narrower than the gutter.
- Every direct child goes in the column. A child with
data-breakoutspans all three tracks. - Both rules have zero specificity, so a child's own
col-*class wins:col-[2/-1]bleeds to one side only. - Put text inside elements. A bare text node cannot be targeted by the column rule, so the grid would place it in a side track.
- Grid items do not collapse their margins. Space the rows with
gap-y-*on the Container. centerhas no effect here — the column is always centred.
CSS variables
Container publishes its geometry on the root, and every descendant inherits it.
| Variable | Set by | Value |
|---|---|---|
--container-size | size | The cap: 28rem … 96rem, or 100% |
--container-px | padding | The gutter per side — per band for responsive |
- A width off the scale. Set the variable:
style={{ "--container-size": "40rem" }}. An inline style beats the size class, and it works in both layouts. On therenderpath Container's style is merged with the host's, never replaced. - A rail to the edge.
-mx-[var(--container-px)] px-[var(--container-px)]cancels the gutter with a negative margin and puts it back as padding, so the first item still lines up with the text. Because it reads the variable, it stays right at every band. - Custom padding. Use the
paddingprop, the variable, or the logical sidesps-*/pe-*. Notpx-*: tailwind-merge treatspx-*and the logical sides as separate groups, so both classes would ship and stylesheet order would pick the winner.
When to use
Use it for the page-level width: the column an app screen, an article, a settings page or a marketing section sits in.
Reach for something else when:
- You want a surface. Container paints nothing. Put a
CardorSurfaceinside it, or a painted section around it. - You want side-by-side panes — a checkout's form and summary, or a docs page's navigation, article and table of contents. That is
GridandSidebar; each pane can hold its own Container. - You want vertical rhythm. Spacing between sections is
StackorSpacer, orpy-*on the section. Container owns the inline axis only. - The content lives in an overlay. A Modal or Drawer sets its own width.
- You reached for Tailwind's
containerclass. Don't combine the two: that utility sets its own per-breakpointmax-width, which races Container's.
Accessibility
- Pattern: none — layout only. Container renders a plain
<div>and adds no role, name or state. Give it meaning withrender:<main>once per page,<article>for a self-contained post, or<section>with a heading oraria-labelwhen it should be a region landmark. - Keyboard: none. It adds no tab stop and handles no keys.
- Reflow. Its width follows the parent, so Container never forces horizontal scrolling on its own at any zoom level (WCAG 1.4.10).
- RTL. Padding uses logical sides, so the gutters mirror under
dir="rtl"with no extra work. - Contrast. Container paints nothing. The plates in the examples on this page pass axe in both themes.
- AT matrix. Chromium's accessibility tree verified: the plain
<div>exposes no role, andrender={<article />}exposes onearticle. VoiceOver, NVDA and JAWS were not separately exercised — Container contributes no role, name, state or focusable node for them to announce.
API Reference
Container
| Prop | Type | Default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "full" | "lg" | Width cap, published as --container-size |
padding | "none" | "xs" | "sm" | "md" | "lg" | "xl" | "responsive" | "responsive" | Inline gutter, published as --container-px |
layout | "block" | "breakout" | "block" | A max-width box, or a three-track grid whose data-breakout children span the full width |
center | boolean | true | Centre the block box; false holds it to the start edge. No effect in breakout |
render | ReactElement | — | Render as another element — <main />, <section />, <article /> |
dir | "ltr" | "rtl" | — | Text direction. Stamped only when set |
className | string | — | Merged last |
style | CSSProperties | — | Merged — the way to set --container-size or --container-px |
Every other HTML attribute — id, aria-*, data-*, event handlers — is forwarded to the root, which carries data-slot="container".
Notes
- On the
renderpath the host's own props win, with three exceptions:classNameandstylemerge, anddirstays the host's unless Container sets one. A ref on the host element is kept alongside Container's. data-slot="container"is stamped on a DOM host only; a composite host keeps its own slot.- No effects and no measurement — safe to render on the server.