Color
Components use semantic tokens (what a color is for), never the raw palette. Each semantic token has a light and a dark value, and every text pairing is tested to meet WCAG AA.
In use
A page is mostly white, with light gray bands for grouping and one yellow band at the top. Red appears only on the main action.
surface.brand · text.onBrand
Themes
surface.subtle · text.muted for supporting copy
surface.page · text.default for reading, and text.link for links.
Show code
import {
Button,
Container,
Heading,
Link,
Logo,
Section,
Stack,
Text,
} from '@jsonresume/ui';
// The three surfaces of a jsonresume.org page, top to bottom: brand
// yellow, light gray, white. Text tokens adapt to each.
export default function ColorSurfaces() {
return (
<>
<Section tone="brand" spacing="sm">
<Container>
<Stack direction="row" justify="between" align="center" wrap gap={3}>
<Logo />
<Text tone="muted">surface.brand · text.onBrand</Text>
</Stack>
</Container>
</Section>
<Section tone="subtle" spacing="sm">
<Container>
<Stack gap={2}>
<Heading level={3}>Themes</Heading>
<Text tone="muted">
surface.subtle · text.muted for supporting copy
</Text>
</Stack>
</Container>
</Section>
<Section spacing="sm">
<Container>
<Stack gap={3}>
<Text>
surface.page · text.default for reading, and{' '}
<Link href="#color">text.link</Link> for links.
</Text>
<Stack direction="row" gap={2}>
<Button variant="primary">action.primary</Button>
<Button>action.neutral</Button>
<Button variant="ink">action.ink</Button>
</Stack>
</Stack>
</Container>
</Section>
</>
);
}Text on yellow
Brand yellow is identical in both themes, so components placed on it switch to ink-on-yellow tokens automatically.
Show code
import {
Button,
Container,
Heading,
Link,
Section,
Stack,
Text,
} from '@jsonresume/ui';
// Brand-yellow surfaces switch their descendants to ink-on-yellow tokens,
// so headings, muted text and links stay readable in light and dark.
export default function ColorOnBrand() {
return (
<Section tone="brand" spacing="md">
<Container>
<Stack gap={3}>
<Heading level={3}>Hosted resumes</Heading>
<Text tone="muted">
Muted text on yellow uses a darker gray to keep 4.5:1.
</Text>
<Text>
Links on yellow stay <Link href="#color">link red</Link>.
</Text>
<Stack direction="row" gap={2}>
<Button variant="primary">Get started</Button>
<Button>Learn more</Button>
</Stack>
</Stack>
</Container>
</Section>
);
}Light and dark
data-theme="dark" on any element re-themes everything inside it. Pages are light unless they opt in.
Show code
import {
Badge,
Button,
Callout,
Card,
CardContent,
CardTitle,
Grid,
Link,
Stack,
Text,
} from '@jsonresume/ui';
import * as styles from './light-dark.css';
function Sample() {
return (
<div className={styles.panel}>
<Card>
<CardContent>
<CardTitle>Pro</CardTitle>
<Text tone="muted" size="xs">
by <Link href="#color">JSON Resume Team</Link>
</Text>
</CardContent>
</Card>
<Stack direction="row" gap={2} wrap>
<Button variant="primary">Publish</Button>
<Button>Preview</Button>
<Badge tone="success">Valid</Badge>
</Stack>
<Callout tone="warning">Deprecated field in use.</Callout>
</div>
);
}
// data-theme scopes the dark tokens to a subtree: the same components,
// re-themed with no extra code.
export default function ColorLightDark() {
return (
<Grid minColumnWidth="16rem" gap={0} stretch>
<div data-theme="light">
<Sample />
</div>
<div data-theme="dark">
<Sample />
</div>
</Grid>
);
}import { style } from '@vanilla-extract/css';
import { vars } from '@jsonresume/ui/tokens';
// The panel paints its own page surface, so each half shows its theme.
export const panel = style({
display: 'grid',
gap: vars.space[4],
padding: vars.space[5],
background: vars.color.surface.page,
color: vars.color.text.default,
});Contrast
Computed from the tokens on every build. The same pairs are enforced by unit tests, so a token change that breaks AA fails CI.
| Text \ Surface | page | subtle | muted | raised |
|---|---|---|---|---|
| text.default | 15.9 AAA | 15.1 AAA | 14.2 AAA | 15.9 AAA |
| text.strong | 17.4 AAA | 16.5 AAA | 15.5 AAA | 17.4 AAA |
| text.muted | 5.3 AA | 5.1 AA | 4.8 AA | 5.3 AA |
| text.link | 7.1 AAA | 6.7 AA | 6.3 AA | 7.1 AAA |
| Text \ Surface | page | subtle | muted | raised |
|---|---|---|---|---|
| text.default | 16.0 AAA | 14.6 AAA | 13.3 AAA | 14.6 AAA |
| text.strong | 18.7 AAA | 17.0 AAA | 15.5 AAA | 17.0 AAA |
| text.muted | 7.4 AAA | 6.8 AA | 6.2 AA | 6.8 AA |
| text.link | 7.5 AAA | 6.8 AA | 6.2 AA | 6.8 AA |
Guidelines
Do
- Use semantic tokens (text.muted, surface.subtle), never palette values, in components.
- Keep one yellow band per page, at the top.
- Reserve red for the single primary action and for links.
- Pair every status color with an icon or words.
Don’t
- Put body text in brand yellow, or yellow on white.
- Use red for decoration or headings.
- Invent a new gray: add a token if one is genuinely missing.
- Rely on color alone to show state.
Surfaces
Backgrounds, from the page to the yellow brand band.
| Token | Light | Dark |
|---|---|---|
color.surface.page --jr-color-surface-page | #ffffff | #121212 |
color.surface.subtle --jr-color-surface-subtle | #f9f9f9 | #1c1c1c |
color.surface.muted --jr-color-surface-muted | #f2f2f2 | #242424 |
color.surface.brand --jr-color-surface-brand | #fff18f | #fff18f |
color.surface.brandSubtle --jr-color-surface-brand-subtle | #fffdf0 | #26230f |
color.surface.raised --jr-color-surface-raised | #ffffff | #1c1c1c |
color.surface.inverse --jr-color-surface-inverse | #1a1a1a | #ffffff |
Text
Contrast is measured against the page surface.
| Token | Light | Dark | Contrast on page |
|---|---|---|---|
color.text.default --jr-color-text-default | #222222 | #ededed | 15.91:1 |
color.text.strong --jr-color-text-strong | #1a1a1a | #ffffff | 17.40:1 |
color.text.muted --jr-color-text-muted | #6b6b6b | #a3a3a3 | 5.33:1 |
color.text.inverse --jr-color-text-inverse | #ffffff | #1a1a1a | 1.00:1 |
color.text.onBrand --jr-color-text-on-brand | #1a1a1a | #1a1a1a | 17.40:1 |
color.text.link --jr-color-text-link | #a92525 | #ff7b7b | 7.05:1 |
color.text.linkHover --jr-color-text-link-hover | #8a1d1d | #ffa3a3 | 9.23:1 |
Borders
| Token | Light | Dark |
|---|---|---|
color.border.default --jr-color-border-default | #dddddd | #333333 |
color.border.subtle --jr-color-border-subtle | #eeeeee | #2a2a2a |
color.border.strong --jr-color-border-strong | #cccccc | #555555 |
Actions
Button fills, borders and labels.
| Token | Light | Dark |
|---|---|---|
color.action.primary --jr-color-action-primary | #d23a3a | #d23a3a |
color.action.primaryHover --jr-color-action-primary-hover | #ce2424 | #ce2424 |
color.action.primaryText --jr-color-action-primary-text | #ffffff | #ffffff |
color.action.ink --jr-color-action-ink | #1a1a1a | #ffffff |
color.action.inkHover --jr-color-action-ink-hover | #333333 | #dddddd |
color.action.inkText --jr-color-action-ink-text | #ffffff | #1a1a1a |
color.action.neutral --jr-color-action-neutral | #ffffff | #1c1c1c |
color.action.neutralHover --jr-color-action-neutral-hover | #f9f9f9 | #242424 |
color.action.neutralText --jr-color-action-neutral-text | #555555 | #d4d4d4 |
color.action.neutralBorder --jr-color-action-neutral-border | #dddddd | #333333 |
color.action.neutralBorderHover --jr-color-action-neutral-border-hover | #999999 | #999999 |
Status
Info, success, warning and danger, each with a tinted surface.
| Token | Light | Dark |
|---|---|---|
color.status.info --jr-color-status-info | #1f4f9e | #8db6ff |
color.status.infoSurface --jr-color-status-info-surface | #eff5ff | #14213a |
color.status.success --jr-color-status-success | #1f6b3e | #6fd39a |
color.status.successSurface --jr-color-status-success-surface | #effaf3 | #12291c |
color.status.warning --jr-color-status-warning | #8a5200 | #ffc966 |
color.status.warningSurface --jr-color-status-warning-surface | #fff8eb | #2e2211 |
color.status.danger --jr-color-status-danger | #a92525 | #ff7b7b |
color.status.dangerSurface --jr-color-status-danger-surface | #fdf3f3 | #2e1515 |
Focus and overlay
| Token | Light | Dark |
|---|---|---|
color.focus --jr-color-focus | #1a1a1a | #fff18f |
| Token | Light | Dark |
|---|---|---|
color.overlay --jr-color-overlay | rgba(17, 17, 17, 0.45) | rgba(0, 0, 0, 0.6) |
Focus
Palette
The raw ramps the semantic tokens are built from. Reach for these only when adding a token.
yellow
gray
red
green
amber
blue
Brand yellow is yellow.200, action red red.500 and link red red.700: the exact values from jsonresume.org.