Skip to content

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.

Contrast checksAll pass 4.5:1
  • 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
Your CSS
: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);
}
Pick a color to re-theme the site.

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.

globals.css
@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.json

Color tokens#

Color tokens
TokenLightDark
--backgroundoklch(0.985 0 0)oklch(0.145 0 0)
--foregroundoklch(0.18 0 0)oklch(0.96 0 0)
--surfaceoklch(1 0 0)oklch(0.18 0 0)
--cardoklch(1 0 0)oklch(0.18 0 0)
--card-foregroundoklch(0.18 0 0)oklch(0.96 0 0)
--popoveroklch(1 0 0)oklch(0.2 0 0)
--popover-foregroundoklch(0.18 0 0)oklch(0.96 0 0)
--secondaryoklch(0.96 0 0)oklch(0.24 0 0)
--secondary-foregroundoklch(0.22 0 0)oklch(0.96 0 0)
--mutedoklch(0.96 0 0)oklch(0.24 0 0)
--muted-foregroundoklch(0.5 0 0)oklch(0.7 0 0)
--accentoklch(0.955 0 0)oklch(0.24 0 0)
--accent-foregroundoklch(0.22 0 0)oklch(0.96 0 0)
--borderoklch(0.91 0 0)oklch(0.27 0 0)
--inputoklch(0.88 0 0)oklch(0.32 0 0)
--border-strongoklch(0.62 0 0)oklch(0.52 0 0)
--overlayoklch(0.18 0 0 / 0.4)oklch(0 0 0 / 0.6)
--primaryoklch(0.55 0.2 260)oklch(0.68 0.17 260)
--primary-foregroundoklch(0.99 0 0)oklch(0.15 0.02 260)
--ringoklch(0.55 0.2 260)oklch(0.68 0.17 260)
--successoklch(0.52 0.14 150)oklch(0.7 0.15 150)
--success-foregroundoklch(0.99 0 0)oklch(0.15 0.03 150)
--success-textoklch(0.47 0.13 150)oklch(0.75 0.15 150)
--warningoklch(0.8 0.16 80)oklch(0.8 0.15 80)
--warning-foregroundoklch(0.25 0.05 75)oklch(0.2 0.04 75)
--warning-textoklch(0.5 0.12 65)oklch(0.82 0.14 80)
--destructiveoklch(0.55 0.21 25)oklch(0.66 0.19 25)
--destructive-foregroundoklch(0.99 0 0)oklch(0.15 0.03 25)
--destructive-textoklch(0.52 0.2 25)oklch(0.72 0.17 25)
--chart-1oklch(0.55 0.2 260)oklch(0.68 0.17 260)
--chart-2oklch(0.65 0.15 180)oklch(0.72 0.13 180)
--chart-3oklch(0.7 0.16 75)oklch(0.78 0.15 75)
--chart-4oklch(0.6 0.2 320)oklch(0.7 0.18 320)
--chart-5oklch(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.

Contrast of every text and background pair
Text on backgroundLightDark
foreground / background18.01:1AA17.62:1AA
foreground / surface18.81:1AA16.74:1AA
card-foreground / card18.81:1AA16.74:1AA
popover-foreground / popover18.81:1AA16.12:1AA
secondary-foreground / secondary15.41:1AA14.65:1AA
accent-foreground / accent15.19:1AA14.65:1AA
muted-foreground / background5.75:1AA7.41:1AA
muted-foreground / surface6.00:1AA7.04:1AA
muted-foreground / muted5.34:1AA6.16:1AA
muted-foreground / card6.00:1AA7.04:1AA
muted-foreground / popover6.00:1AA6.78:1AA
muted-foreground / accent5.26:1AA6.16:1AA
success-text / card6.42:1AA8.96:1AA
destructive-text / card6.12:1AA7.04:1AA
primary-foreground / primary4.87:1AA6.72:1AA
primary / background4.80:1AA6.76:1AA
primary / surface5.02:1AA6.43:1AA
success-foreground / success5.02:1AA7.79:1AA
warning-foreground / warning8.47:1AA9.58:1AA
destructive-foreground / destructive5.26:1AA5.81:1AA
success-text / background6.15:1AA9.43:1AA
success-text / surface6.42:1AA8.96:1AA
warning-text / background5.93:1AA11.19:1AA
warning-text / surface6.19:1AA10.63:1AA
destructive-text / background5.86:1AA7.41:1AA
destructive-text / surface6.12:1AA7.04:1AA
success-text / popover6.42:1AA8.63:1AA
warning-text / popover6.19:1AA10.23:1AA
destructive-text / popover6.12:1AA6.78:1AA
primary / popover5.02:1AA6.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.

Type scale
TokenSizeSample
--text-xs0.75remShip it
--text-sm0.875remShip it
--text-base1remShip it
--text-lg1.125remShip it
--text-xl1.25remShip it
--text-2xl1.5remShip it
--text-3xl1.875remShip it
--text-4xl2.25remShip it
--text-5xl3remShip it

Space, shape and depth#

Space, radius, shadow and layer tokens
TokenValue
--spacing0.25rem
--radius0.5rem
--radius-sm0.375rem
--radius-xs0.25rem
--radius-pill9999px
--shadow-float0 8px 24px -8px oklch(0 0 0 / 0.12), 0 2px 6px -2px oklch(0 0 0 / 0.08)
--shadow-overlay0 24px 48px -12px oklch(0 0 0 / 0.18), 0 4px 12px -4px oklch(0 0 0 / 0.08)
--z-dropdown50
--z-sticky60
--z-overlay100
--z-toast200
Depth comes from borders. Shadows are only for floating things: menus, popovers, tooltips and toasts.

Motion#

Motion tokens
TokenValue
--duration-fast120ms
--duration-base200ms
--duration-slow360ms
--ease-standardcubic-bezier(0.2, 0, 0, 1)
--ease-entercubic-bezier(0, 0, 0, 1)
--ease-exitcubic-bezier(0.3, 0, 1, 1)
--ease-springcubic-bezier(0.34, 1.56, 0.64, 1)
--stagger40ms
--motion-scale1

Every duration is multiplied by --motion-scale, which the data-motion switch and prefers-reduced-motion turn down. See Dark mode and motion.