Skip to content

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.

Surfaces and their text
JSON Resume

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
color/surfaces.tsx
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.

Section tone="brand"

Hosted resumes

Muted text on yellow uses a darker gray to keep 4.5:1.

Links on yellow stay link red.

Show code
color/on-brand.tsx
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.

The same components in both themes
Valid
Deprecated field in use.
Valid
Deprecated field in use.
Show code
color/light-dark.tsx
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>
  );
}
color/light-dark.css.ts
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.

Light theme. AA needs 4.5:1 for body text.
Text \ Surfacepagesubtlemutedraised
text.default15.9 AAA15.1 AAA14.2 AAA15.9 AAA
text.strong17.4 AAA16.5 AAA15.5 AAA17.4 AAA
text.muted5.3 AA5.1 AA4.8 AA5.3 AA
text.link7.1 AAA6.7 AA6.3 AA7.1 AAA
Dark theme. AA needs 4.5:1 for body text.
Text \ Surfacepagesubtlemutedraised
text.default16.0 AAA14.6 AAA13.3 AAA14.6 AAA
text.strong18.7 AAA17.0 AAA15.5 AAA17.0 AAA
text.muted7.4 AAA6.8 AA6.2 AA6.8 AA
text.link7.5 AAA6.8 AA6.2 AA6.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.

TokenLightDark
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.

TokenLightDarkContrast on page
color.text.default
--jr-color-text-default
#222222#ededed15.91:1
color.text.strong
--jr-color-text-strong
#1a1a1a#ffffff17.40:1
color.text.muted
--jr-color-text-muted
#6b6b6b#a3a3a35.33:1
color.text.inverse
--jr-color-text-inverse
#ffffff#1a1a1a1.00:1
color.text.onBrand
--jr-color-text-on-brand
#1a1a1a#1a1a1a17.40:1
color.text.link
--jr-color-text-link
#a92525#ff7b7b7.05:1
color.text.linkHover
--jr-color-text-link-hover
#8a1d1d#ffa3a39.23:1

Borders

TokenLightDark
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.

TokenLightDark
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.

TokenLightDark
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

TokenLightDark
color.focus
--jr-color-focus
#1a1a1a#fff18f
TokenLightDark
color.overlay
--jr-color-overlay
rgba(17, 17, 17, 0.45)rgba(0, 0, 0, 0.6)

Focus

Focus is ink in light mode and brand yellow in dark mode, drawn as a 2px outline with a 2px offset. It is never removed.

Palette

The raw ramps the semantic tokens are built from. Reach for these only when adding a token.

yellow

yellow.50#fffdf0
yellow.100#fff9cf
yellow.200#fff18f
yellow.300#ffe95c
yellow.400#f2d935
yellow.700#6b5f0e

gray

gray.25#fcfcfc
gray.50#f9f9f9
gray.100#f2f2f2
gray.150#eeeeee
gray.200#dddddd
gray.300#cccccc
gray.400#999999
gray.500#6b6b6b
gray.600#555555
gray.700#333333
gray.800#222222
gray.850#1c1c1c
gray.900#1a1a1a
gray.950#121212

red

red.50#fdf3f3
red.100#fbe2e2
red.300#ff7b7b
red.500#d23a3a
red.600#ce2424
red.700#a92525
red.800#8a1d1d

green

green.50#effaf3
green.300#6fd39a
green.700#1f6b3e

amber

amber.50#fff8eb
amber.300#ffc966
amber.700#8a5200

blue

blue.50#eff5ff
blue.300#8db6ff
blue.700#1f4f9e

Brand yellow is yellow.200, action red red.500 and link red red.700: the exact values from jsonresume.org.