Skip to content

Theming

Scintillar uses Tailwind CSS v4 + CSS variables for theming, the same system as shadcn/ui. Every color, radius, font, and shadow used by the components resolves to a CSS variable, so you can rebrand the entire registry by editing one file.

The token file

All design tokens live in app/globals.css. Two selectors define the light and dark palettes:

:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --primary: oklch(0.426 0.094 161.619);
  --primary-foreground: oklch(0.985 0 0);
  --border: oklch(0.922 0 0);
  --radius: 0.625rem;
  /* ...more */
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --primary: oklch(0.796 0.125 168.082);
  /* ...more */
}

The @theme inline block at the top of the file maps every CSS variable to a Tailwind utility (e.g. --color-primarybg-primary, text-primary, border-primary). You don't need to edit that block — adding or changing a --* variable is enough.

Customizing the primary color

Change the brand color by editing the two --primary declarations (one in :root, one in .dark). Use oklch values for perceptual uniformity:

:root {
  --primary: oklch(0.55 0.19 263);  /* indigo */
}
.dark {
  --primary: oklch(0.72 0.18 263);
}

The hero card border, the active section tab underline, the data-table row-flash animations, the bento chart, the live cursors — every primary-tinted accent on the homepage updates immediately, no rebuild needed in dev mode.

Dark mode

Dark mode is wired via next-themes. The provider lives at components/theme-provider.tsx:

<NextThemesProvider
  attribute="class"
  defaultTheme="system"
  enableSystem
  disableTransitionOnChange
>
  {children}
</NextThemesProvider>

disableTransitionOnChange is important — without it, every CSS transition fires when the user toggles themes, causing a full-page flicker. The header's <ThemeToggle /> calls setTheme("dark" | "light" | "system") from the useTheme() hook.

Adding a custom color token

To add a new token (say, a "warning" color), add it to both :root and .dark, then expose it under @theme inline:

@theme inline {
  --color-warning: var(--warning);
  --color-warning-foreground: var(--warning-foreground);
}

:root {
  --warning: oklch(0.78 0.18 75);
  --warning-foreground: oklch(0.145 0 0);
}
.dark {
  --warning: oklch(0.85 0.16 75);
  --warning-foreground: oklch(0.145 0 0);
}

You can now use bg-warning text-warning-foreground anywhere.

Fonts

Fonts are loaded with next/font/google in app/layout.tsx:

import { Space_Grotesk, JetBrains_Mono } from "next/font/google"

const spaceGrotesk = Space_Grotesk({ variable: "--font-sans", subsets: ["latin"] })
const spaceMono = JetBrains_Mono({ variable: "--font-mono", weight: ["400", "700"], subsets: ["latin"] })

The variables --font-sans and --font-mono are then picked up by @theme inline (--default-font-family / --default-mono-font-family) and applied via Tailwind's font-sans / font-mono utilities. To swap fonts, replace the imports — every font-sans/font-mono consumer follows automatically.

Block-level overrides

All complex blocks (DataTable, ProfileForm, OrgRolesForm, etc.) accept a className prop that's merged onto the root container, plus a compact boolean that swaps in tighter padding/spacing. The bento card on the homepage uses both:

<ProfileForm
  compact
  className="h-full bg-card/85 backdrop-blur-sm rounded-xl border-border/60"
  /* ... */
/>

If you need finer control (e.g. swap a background gradient on a card), start from the card classes above and layer your own bg-* / border-* utilities on top.