Getting started
Theming
Every value is a CSS variable, light and dark. Change --primary and the app follows.
Pick an accent#
Choose a hue and Junco picks the lightness that keeps text readable at 4.5:1 in both light and dark. The whole site re-themes as you drag.
- Text on primary (light)4.67:1 pass
- Primary on page (light)4.60:1 pass
- Primary on surface (light)4.81:1 pass
- Text on primary (dark)4.88:1 pass
- Primary on page (dark)4.91:1 pass
- Primary on surface (dark)4.66:1 pass
:root {
--primary: oklch(0.56 0.200 260);
--primary-foreground: oklch(0.99 0 0);
--ring: oklch(0.56 0.200 260);
}
.dark,
[data-theme="dark"] {
--primary: oklch(0.60 0.180 260);
--primary-foreground: oklch(0.15 0.030 260);
--ring: oklch(0.60 0.180 260);
}How it works#
Tokens live in @junco/tokens as plain CSS variables. Light values sit on :root; dark values on .dark and [data-theme="dark"]. @junco/tailwind maps each one to a utility, so --muted-foreground becomes text-muted-foreground. Names follow shadcn/ui, so shadcn themes drop straight in.
@import "tailwindcss";
@import "@junco/tailwind";
:root { --primary: oklch(0.55 0.2 150); }
.dark { --primary: oklch(0.72 0.17 150); }Tokens also ship as a TypeScript object and as W3C design token JSON, for Figma and other tools:
import { tokens } from "@junco/tokens";
tokens.light.primary; // "oklch(0.55 0.2 260)"
// W3C (DTCG) JSON: @junco/tokens/tokens.jsonColor tokens#
| Token | Light | Dark |
|---|---|---|
--background | oklch(0.985 0 0) | oklch(0.145 0 0) |
--foreground | oklch(0.18 0 0) | oklch(0.96 0 0) |
--surface | oklch(1 0 0) | oklch(0.18 0 0) |
--card | oklch(1 0 0) | oklch(0.18 0 0) |
--card-foreground | oklch(0.18 0 0) | oklch(0.96 0 0) |
--popover | oklch(1 0 0) | oklch(0.2 0 0) |
--popover-foreground | oklch(0.18 0 0) | oklch(0.96 0 0) |
--secondary | oklch(0.96 0 0) | oklch(0.24 0 0) |
--secondary-foreground | oklch(0.22 0 0) | oklch(0.96 0 0) |
--muted | oklch(0.96 0 0) | oklch(0.24 0 0) |
--muted-foreground | oklch(0.5 0 0) | oklch(0.7 0 0) |
--accent | oklch(0.955 0 0) | oklch(0.24 0 0) |
--accent-foreground | oklch(0.22 0 0) | oklch(0.96 0 0) |
--border | oklch(0.91 0 0) | oklch(0.27 0 0) |
--input | oklch(0.88 0 0) | oklch(0.32 0 0) |
--border-strong | oklch(0.62 0 0) | oklch(0.52 0 0) |
--overlay | oklch(0.18 0 0 / 0.4) | oklch(0 0 0 / 0.6) |
--primary | oklch(0.55 0.2 260) | oklch(0.68 0.17 260) |
--primary-foreground | oklch(0.99 0 0) | oklch(0.15 0.02 260) |
--ring | oklch(0.55 0.2 260) | oklch(0.68 0.17 260) |
--success | oklch(0.52 0.14 150) | oklch(0.7 0.15 150) |
--success-foreground | oklch(0.99 0 0) | oklch(0.15 0.03 150) |
--success-text | oklch(0.47 0.13 150) | oklch(0.75 0.15 150) |
--warning | oklch(0.8 0.16 80) | oklch(0.8 0.15 80) |
--warning-foreground | oklch(0.25 0.05 75) | oklch(0.2 0.04 75) |
--warning-text | oklch(0.5 0.12 65) | oklch(0.82 0.14 80) |
--destructive | oklch(0.55 0.21 25) | oklch(0.66 0.19 25) |
--destructive-foreground | oklch(0.99 0 0) | oklch(0.15 0.03 25) |
--destructive-text | oklch(0.52 0.2 25) | oklch(0.72 0.17 25) |
--chart-1 | oklch(0.55 0.2 260) | oklch(0.68 0.17 260) |
--chart-2 | oklch(0.65 0.15 180) | oklch(0.72 0.13 180) |
--chart-3 | oklch(0.7 0.16 75) | oklch(0.78 0.15 75) |
--chart-4 | oklch(0.6 0.2 320) | oklch(0.7 0.18 320) |
--chart-5 | oklch(0.6 0.15 30) | oklch(0.7 0.14 30) |
Contrast#
Every text and background pair Junco uses is checked by a test at 4.5:1 (WCAG AA) in both modes. If a token change breaks a pair, the build fails. Status colors have separate -text tokens for text on the page, because a solid amber that works as a fill can never pass as text on white.
| Text on background | Light | Dark |
|---|---|---|
foreground / background | 18.01:1AA | 17.62:1AA |
foreground / surface | 18.81:1AA | 16.74:1AA |
card-foreground / card | 18.81:1AA | 16.74:1AA |
popover-foreground / popover | 18.81:1AA | 16.12:1AA |
secondary-foreground / secondary | 15.41:1AA | 14.65:1AA |
accent-foreground / accent | 15.19:1AA | 14.65:1AA |
muted-foreground / background | 5.75:1AA | 7.41:1AA |
muted-foreground / surface | 6.00:1AA | 7.04:1AA |
muted-foreground / muted | 5.34:1AA | 6.16:1AA |
muted-foreground / card | 6.00:1AA | 7.04:1AA |
muted-foreground / popover | 6.00:1AA | 6.78:1AA |
muted-foreground / accent | 5.26:1AA | 6.16:1AA |
success-text / card | 6.42:1AA | 8.96:1AA |
destructive-text / card | 6.12:1AA | 7.04:1AA |
primary-foreground / primary | 4.87:1AA | 6.72:1AA |
primary / background | 4.80:1AA | 6.76:1AA |
primary / surface | 5.02:1AA | 6.43:1AA |
success-foreground / success | 5.02:1AA | 7.79:1AA |
warning-foreground / warning | 8.47:1AA | 9.58:1AA |
destructive-foreground / destructive | 5.26:1AA | 5.81:1AA |
success-text / background | 6.15:1AA | 9.43:1AA |
success-text / surface | 6.42:1AA | 8.96:1AA |
warning-text / background | 5.93:1AA | 11.19:1AA |
warning-text / surface | 6.19:1AA | 10.63:1AA |
destructive-text / background | 5.86:1AA | 7.41:1AA |
destructive-text / surface | 6.12:1AA | 7.04:1AA |
success-text / popover | 6.42:1AA | 8.63:1AA |
warning-text / popover | 6.19:1AA | 10.23:1AA |
destructive-text / popover | 6.12:1AA | 6.78:1AA |
primary / popover | 5.02:1AA | 6.19:1AA |
Type#
Geist for UI and headings, Geist Mono for code and numbers. text-sm (14px) is the UI default; body copy is text-base.
| Token | Size | Sample |
|---|---|---|
--text-xs | 0.75rem | Ship it |
--text-sm | 0.875rem | Ship it |
--text-base | 1rem | Ship it |
--text-lg | 1.125rem | Ship it |
--text-xl | 1.25rem | Ship it |
--text-2xl | 1.5rem | Ship it |
--text-3xl | 1.875rem | Ship it |
--text-4xl | 2.25rem | Ship it |
--text-5xl | 3rem | Ship it |
Space, shape and depth#
| Token | Value |
|---|---|
--spacing | 0.25rem |
--radius | 0.5rem |
--radius-sm | 0.375rem |
--radius-xs | 0.25rem |
--radius-pill | 9999px |
--shadow-float | 0 8px 24px -8px oklch(0 0 0 / 0.12), 0 2px 6px -2px oklch(0 0 0 / 0.08) |
--shadow-overlay | 0 24px 48px -12px oklch(0 0 0 / 0.18), 0 4px 12px -4px oklch(0 0 0 / 0.08) |
--z-dropdown | 50 |
--z-sticky | 60 |
--z-overlay | 100 |
--z-toast | 200 |
Motion#
| Token | Value |
|---|---|
--duration-fast | 120ms |
--duration-base | 200ms |
--duration-slow | 360ms |
--ease-standard | cubic-bezier(0.2, 0, 0, 1) |
--ease-enter | cubic-bezier(0, 0, 0, 1) |
--ease-exit | cubic-bezier(0.3, 0, 1, 1) |
--ease-spring | cubic-bezier(0.34, 1.56, 0.64, 1) |
--stagger | 40ms |
--motion-scale | 1 |
Every duration is multiplied by --motion-scale, which the data-motion switch and prefers-reduced-motion turn down. See Dark mode and motion.