OTFotf

Tokens overview

The @otfdashkit/tokens design system — CSS variables, palettes, and runtime theme switching.

@otfdashkit/tokens is the source-of-truth for every visual decision in OTF. It ships as CSS custom properties (consumed by @otfdashkit/ui) and as a JS object (consumed by @otfdashkit/ui-native via Tamagui). One source, two outputs.

What's in it

Token bucketExamples
Color--background, --foreground, --primary, --card, --border, --ring, --chart-1..5
Typography--font-sans, --font-heading, --font-mono, --font-size-{xs..4xl}, --font-weight-*, --line-height-*, --letter-spacing-*
Border radius--radius, --radius-sm, --radius-md, --radius-lg, --radius-xl
Spacing scaleImplicit via Tailwind 4 + the OTF preset
MotionDefault ease curves + duration tokens

Every component reads exclusively from these variables. Never hardcode hex outside the tokens package@otfdashkit/eslint-plugin-otf-design will flag it.

Web setup

Import the CSS once in your app's globals, before the component CSS:

app/globals.css
@import '@otfdashkit/tokens/web.css';
@import '@otfdashkit/ui/styles';

That's it. The tokens cascade onto :root and every component picks them up.

Native setup

Import the JS export inside your Tamagui config:

tamagui.config.ts
import { tokens, themes } from '@otfdashkit/tokens/native'
import { createTamagui } from '@otfdashkit/ui-native'

export default createTamagui({
  tokens,
  themes,
})

The four shipped themes

OTF ships four palettes. Each has a light + dark variant.

ClassVibeWhen to use
theme-slateCool slate, neutral graysDefault — works for almost any product
theme-warmWarm purple, cream backgroundsLifestyle / wellness / creator products
theme-cosmicMagenta-violet on near-blackEditorial / showcase / portfolio
theme-terminalPure mono — black on greenDeveloper tools / terminal-shaped apps

The fitness-kit defaults to its own brand override (warm orange) which is scoped to :root:not([class*="theme-"]) — i.e. it's the fallback when no theme-* class is set. Switching to any of the four shipped themes overrides the orange brand.

Switching themes at runtime

Set the class on <html>:

document.documentElement.className = 'theme-warm dark'

For zero-flicker first paint, do this before React hydrates:

app/layout.tsx
<script dangerouslySetInnerHTML={{ __html: `
  const t = localStorage.getItem('otf-theme') || 'theme-slate'
  const s = localStorage.getItem('otf-color-scheme') || 'dark'
  document.documentElement.className = \`\${t} \${s}\`
` }} />

The kits use <FloatingThemePicker /> (rendered globally) and <ThemeSwitch /> inside Settings to persist + apply the choice.

Authoring a custom palette

Copy any of the four shipped palette blocks from packages/tokens/src/web.css and rename:

.theme-mybrand {
  --background:        0 0% 100%;
  --foreground:        220 8% 12%;
  --primary:           212 100% 47%;
  --primary-foreground: 0 0% 100%;
  --card:              0 0% 100%;
  --card-foreground:   220 8% 12%;
  --border:            220 8% 92%;
  --ring:              212 100% 47%;
  --chart-1:           212 100% 47%;
  --chart-2:           28 100% 56%;
  --chart-3:           160 70% 40%;
  --chart-4:           340 80% 56%;
  --chart-5:           260 60% 56%;
}

.theme-mybrand.dark,
.theme-mybrand .dark {
  --background:        220 12% 7%;
  --foreground:        0 0% 96%;
  --primary:           212 100% 60%;
  /* … */
}

Drop the file into your app's CSS, set document.documentElement.className = 'theme-mybrand dark', and every OTF component re-paints to the new palette.

Tailwind preset

For Tailwind 4 projects, import the OTF preset to get token-aware utility classes (bg-background, text-foreground, border-border, bg-card, bg-primary, …):

tailwind.config.ts
import otfPreset from '@otfdashkit/tokens/tailwind-preset'

export default {
  presets: [otfPreset],
  content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
}

The kits use these utility classes exclusively — never inline style={{ background: '#...' }}.

Charts

Charts always use chart tokens:

<BarChart
  data={data}
  dataKey={['issues', 'completed']}
  colors={['hsl(var(--chart-1))', 'hsl(var(--chart-3))']}
  stacked
/>

Each palette ships a chart-1 through chart-5. They're tuned for legibility against the palette's background, both light and dark.

On this page