VexellDocs · v1.5

Getting started

Customization

Every visual property is a CSS variable. Change one token and the whole template re-themes — no rebuild, no JS config object. The same tokens live in both editions, so brand + theme decisions port across.

Brand color

The brand ramp is a 10-step oklch() scale. Edit the file directly, or open /themes in either edition for an interactive hue slider that exports a paste-ready @theme block.

css
/* tokens/theme.css */
@theme {
  --color-brand-50:  oklch(0.98 0.015 280);   /* hue=280 → indigo */
  --color-brand-100: oklch(0.95 0.030 280);
  --color-brand-200: oklch(0.90 0.060 280);
  --color-brand-300: oklch(0.83 0.110 280);
  --color-brand-400: oklch(0.72 0.180 280);
  --color-brand-500: oklch(0.62 0.230 280);
  --color-brand-600: oklch(0.55 0.250 280);   /* primary CTA */
  --color-brand-700: oklch(0.46 0.220 280);
  --color-brand-800: oklch(0.38 0.190 280);
  --color-brand-900: oklch(0.30 0.160 280);
}

/* Tip: change the third value (the hue) across all 10 lines and the
   whole template re-themes. Try 200 (teal), 30 (orange), 340 (pink). */

Tip — live customizer: open /themes in your edition. Drag the hue slider, copy the generated @theme block, paste it over the brand block above. Done in 30 seconds.

Typography

Geist Variable ships at three weights — 400 / 500 / 600. Vexell enforces a single weight ladder for visual coherence.

css
/* tokens/theme.css */
@theme {
  --font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif;
  --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
  --font-display: var(--font-sans);
}

/* To swap the family, drop your woff2 files into fonts/ and update
   the @font-face declaration in tokens/index.css. */

Dark mode

Class-based with system fallback. The html element gets the dark class when the user picked dark explicitly, or when they picked "system" and the OS prefers dark.

Both editions ship an inline anti-flash script that runs synchronously before paint, so there's no light flash on first load.

tsx
// app/layout.tsx — already in place. Same anti-flash script.
// React 19 + App Router safely renders the script before hydration.

const antiFlashScript = `
  (function () {
    var stored = localStorage.getItem('theme')
    if (stored === 'dark' ||
        (stored !== 'light' && matchMedia('(prefers-color-scheme: dark)').matches)) {
      document.documentElement.classList.add('dark')
    }
  })()
`

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <script dangerouslySetInnerHTML={{ __html: antiFlashScript }} />
      </head>
      <body>{children}</body>
    </html>
  )
}

Read next