Layout
A single 970px column with 30px gutters carries most pages, inside full-width bands. Long text keeps a 72-character measure.
Page structure
Every page follows the same skeleton.
Themes
Browse the community gallery.
Content sits in the Container.
Grids reflow inside it.
Bands span the full width.
Show code
layout/page-structure.tsx
import {
Container,
Grid,
Logo,
PageHeader,
Section,
SiteFooter,
SiteHeader,
SiteHeaderLink,
Text,
} from '@jsonresume/ui';
// Every page: yellow header, gray title band, content in the 970px column,
// quiet footer.
export default function LayoutPageStructure() {
return (
<div>
<SiteHeader brand={<Logo />} navLabel="Example site">
<SiteHeaderLink href="#layout">Schema</SiteHeaderLink>
<SiteHeaderLink href="#layout" active>
Themes
</SiteHeaderLink>
</SiteHeader>
<PageHeader title="Themes" description="Browse the community gallery." />
<Section>
<Container>
<Grid minColumnWidth="12rem" gap={6}>
<Text>Content sits in the Container.</Text>
<Text>Grids reflow inside it.</Text>
<Text>Bands span the full width.</Text>
</Grid>
</Container>
</Section>
<SiteFooter note="JSON Resume is open source" />
</div>
);
}Intrinsic grids
Grids decide their own column count from a minimum width, so most layouts need no breakpoints at all.
Theme 1
Theme 2
Theme 3
Theme 4
Theme 5
Theme 6
Show code
layout/responsive-grid.tsx
'use client';
import { Card, CardContent, Grid, Stack, Text } from '@jsonresume/ui';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import { useId, useState } from 'react';
import * as styles from './responsive-grid.css';
// Grid fits as many 10rem columns as the space allows: drag the slider to
// see it reflow with no breakpoints.
export default function LayoutResponsiveGrid() {
const [percent, setPercent] = useState(100);
const id = useId();
return (
<Stack gap={4}>
<Stack direction="row" gap={3} align="center">
<label htmlFor={id}>
<Text as="span" size="xs" tone="muted">
Container width: {percent}%
</Text>
</label>
<input
id={id}
type="range"
min={30}
max={100}
value={percent}
onChange={(e) => setPercent(Number(e.target.value))}
/>
</Stack>
<div
className={styles.frame}
style={assignInlineVars({ [styles.width]: `${percent}%` })}
>
<Grid minColumnWidth="10rem" gap={4}>
{Array.from({ length: 6 }, (_, i) => (
<Card key={i} tone="subtle">
<CardContent>Theme {i + 1}</CardContent>
</Card>
))}
</Grid>
</div>
</Stack>
);
}layout/responsive-grid.css.ts
import { createVar, style } from '@vanilla-extract/css';
import { vars } from '@jsonresume/ui/tokens';
/** The container width, set from the slider. */
export const width = createVar();
export const frame = style({
width,
padding: vars.space[2],
outline: `1px dashed ${vars.color.border.strong}`,
vars: { [width]: '100%' },
});Widths and gutters
| Token | Value |
|---|---|
| layout.container | 970px |
| layout.wide | 1200px |
| layout.prose | 72ch |
| layout.gutter | 30px |
| layout.gutterNarrow | 16px |
Breakpoints
Mobile first: styles apply from each width up. Exported as numbers and media query strings.
| Token | Value |
|---|---|
| breakpoints.sm | 600 |
| breakpoints.md | 768 |
| breakpoints.lg | 992 |
| breakpoints.xl | 1200 |
Prefer intrinsic layouts (Grid with a minimum column width, wrapping Stacks) over breakpoints. Everything must reflow at 320px without horizontal scrolling.
Layers
| Token | Value |
|---|---|
| zIndex.dropdown | 1000 |
| zIndex.sticky | 1100 |
| zIndex.overlay | 1300 |
| zIndex.modal | 1400 |
| zIndex.toast | 1500 |
| zIndex.tooltip | 1600 |