Theming & Styling

Customize Tailwind CSS v4 tokens, dark mode, and shadcn/ui in the TanStack template.

The template uses Tailwind CSS v4, semantic CSS variables, next-themes, and shadcn/ui. Despite its package name, next-themes is used here as a framework- agnostic React theme controller; this is a TanStack Start application.

Styling stack

FileRole
vite.config.tsRegisters @tailwindcss/vite
src/styles.cssTailwind import, dark variant, design tokens, Fumadocs mapping, typography, animation, layout utilities
src/components/theme-provider.tsxConfigures next-themes class mode, system default, and theme storage key
src/routes/__root.tsxRuns the pre-hydration theme script and mounts ThemeProvider
src/components/common/ThemeToggle.tsxTheme control in marketing UI
components.jsonshadcn style, aliases, icon library, and CSS entrypoint
src/lib/utils.tscn() class merge helper

Tailwind is a Vite plugin, so there is no Tailwind config file in the default setup. src/styles.css imports Tailwind and exposes semantic tokens through @theme inline.

Semantic colors

Components use roles such as bg-background, text-foreground, bg-card, text-muted-foreground, border-border, and ring-ring. The corresponding variables live in :root and .dark.

:root {
	--background: #f8fafc;
	--foreground: #0f172a;
	--primary: #3b76f6;
	--border: #e2e8f0;
}

.dark {
	--background: #090807;
	--foreground: #f8fafc;
	--primary: #3b76f6;
}

Change token values before replacing semantic utilities in individual components. src/styles.css also maps the Fumadocs --color-fd-* variables to the same system so docs and application surfaces stay coherent.

components.json declares the shadcn new-york style with a Zinc base and CSS variables. The shipped stylesheet has since customized the visible palette with slate surfaces, brand blue, and a warm dark canvas; treat the semantic variables as the current source of truth.

Dark mode without a flash

ThemeProvider stores light, dark, or system under the theme key and applies the resolved light/dark class to <html>. The inline script in __root.tsx reads that value before React hydrates, resolves system preference, sets colorScheme, and records the raw preference in data-theme.

The root <html> uses suppressHydrationWarning because the script may change the class before React attaches. Fumadocs' own theme controller is disabled so there is a single theme owner.

Keep the inline script and ThemeProvider settings synchronized: storage key, accepted values, default theme, class attribute, and system behavior must agree. A mismatch produces a flash or hydration warning.

Add a shadcn component

The project is not an RSC app ("rsc": false). shadcn aliases resolve to #/components, #/components/ui, #/lib, and #/hooks.

pnpm dlx shadcn@latest add alert-dialog

Review generated imports and keep reusable primitives in src/components/ui. Compose feature UI in the domain directories (auth, home, dashboard, layout, ai) and animations in motion.

Use cn() for conditional utilities and conflict resolution:

import { cn } from "#/lib/utils"

<div className={cn("rounded-lg border", active && "border-primary")} />

Code style

Biome formats TypeScript/TSX with tabs and double quotes; generated src/routeTree.gen.ts and the hand-curated stylesheet are excluded from its normal formatting scope. Code examples and new configuration should follow the template's tab indentation and #/* aliases.

Verify a theme change

  • Light and dark text, surfaces, borders, inputs, focus rings, charts, and destructive states meet contrast needs.
  • Refreshing restores light, dark, and system without a visible flash.
  • The browser console has no hydration warning caused by the theme class.
  • Fumadocs pages use the same brand tokens as marketing and dashboard pages.
  • New shadcn components use semantic colors and work with keyboard/focus states.
  • pnpm check and pnpm build pass after component generation.

On this page