Styling & Theming
This template uses Tailwind CSS 4.1, shadcn/ui (Radix-powered), CVA for variants, and tailwind-merge for safe class merging.
Design tokens are exposed via CSS variables and mapped to Tailwind using the @theme inline block.
Theme Provider
Light/Dark mode is driven by src/providers/theme-provider.tsx and a simple hook:
// Toggle theme example
import { useTheme } from "@/hooks/use-theme"
const { theme, setTheme } = useTheme()
setTheme(theme === "dark" ? "light" : "dark")- The provider adds/removes the
.darkclass on<html>and syncs with system preference. - Components read colors from semantic tokens (e.g.
bg-card,text-foreground).
Token System (CSS Variables → Tailwind)
Tokens are defined in index.css using :root (light) and .dark (dark).
Then they are mapped to Tailwind tokens in @theme inline.
- Define raw variables:
:root {
--background: #fff;
--foreground: #0a0a0a;
--primary: #1c67f3;
/* ... */
}
.dark {
--background: #121316;
--foreground: #fafafa;
--primary: #1c67f3;
/* ... */
}- Expose to Tailwind:
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
/* ... more mappings ... */
}Result: you can use semantic utilities like
bg-background,text-foreground,bg-card,border-borderacross the app. Switching theme simply flips the underlying CSS variables.
Dark Variant (Tailwind 4)
We enable a custom dark variant to target inner scopes:
@custom-variant dark (&:is(.dark *));Usage examples:
<div className="bg-card dark:bg-card">...</div>
<p className="text-foreground/70 dark:text-foreground/80">...</p>shadcn/ui
All primitives in src/components/ui are generated/structured like shadcn components:
- Accessible by default (keyboard/focus, ARIA).
- Theme-aware via Tailwind tokens.
- Extendable with
classNameand CVA variants.
Add new primitives:
npx shadcn@latest add button
# or any other component nameComponent Variants with CVA
Use CVA to declare design-system variants and sizes in a single source of truth.
// src/components/ui/button.tsx (excerpt)
import { cva, type VariantProps } from "class-variance-authority"
import { twMerge } from "tailwind-merge"
export const buttonStyles = cva(
"inline-flex items-center justify-center rounded-2xl font-medium transition-colors disabled:opacity-50 disabled:pointer-events-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:opacity-95",
ghost: "bg-transparent text-foreground hover:bg-card",
outline: "border border-border bg-background hover:bg-card",
link: "underline-offset-4 hover:underline text-primary",
},
size: {
sm: "h-9 px-3 text-sm",
md: "h-10 px-4 text-sm",
lg: "h-11 px-5 text-base",
},
tone: {
success: "bg-success text-success-foreground",
warning: "bg-warning text-warning-foreground",
destructive: "bg-destructive text-destructive-foreground",
},
},
compoundVariants: [
{ variant: "outline", tone: "destructive", class: "border-destructive-border" },
],
defaultVariants: { variant: "default", size: "md" },
}
)
export type ButtonVariants = VariantProps<typeof buttonStyles>
export function cn(...classes: (string | undefined)[]) {
return twMerge(classes.filter(Boolean).join(" "))
}Use it in a component:
import { buttonStyles } from "@/components/ui/button"
<button className={buttonStyles({ variant: "outline", size: "lg" })}>
Continue
</button>Why CVA + tailwind-merge?
- CVA gives typed variants (great DX).
tailwind-mergeprevents conflicting utilities (e.g.px-2vspx-4).
Project Fonts & Base Styles
Fonts are imported in index.css:
@import url('https://fonts.googleapis.com/css2?family=Inria+Serif:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap');
@import "tailwindcss";
@import "tw-animate-css";
.font-second { font-family: var(--second-font-family); }Base resets and typography scale live in @layer base:
@layer base {
* { @apply border-border outline-ring/50; }
html { @apply h-full; scroll-behavior: smooth; }
body { @apply bg-layout text-foreground min-h-full; }
h1 { @apply text-xl md:text-2xl lg:text-3xl; }
p { @apply text-sm md:text-base text-foreground; }
}Utility Add-ons
Custom utilities (e.g., extra font sizes) can be defined in @layer utilities:
@layer utilities {
.text-2xs { font-size: 11px; }
}Example: Themed Card
export function Card({ children }: { children: React.ReactNode }) {
return (
<div className="rounded-xl bg-card text-card-foreground shadow-[0_1px_2px_0_var(--color-shadow-2),0_2px_12px_-2px_var(--color-shadow-12)]">
{children}
</div>
)
}- Uses
bg-card&text-card-foreground(mapped to CSS vars). - Shadow tokens come from
--color-shadow-*to keep elevation consistent.
Tips & Conventions
- Keep
/src/components/uiminimal (shadcn primitives). Create feature UI in/components/custom. - Add new tokens in
:root+.dark, then map under@theme inline. - Prefer semantic utilities (
bg-card,text-foreground) over raw colors. - Variants first: use CVA to encode states (size, tone, emphasis) instead of scattering classes.
- Don’t edit generated shadcn code heavily — extend via wrappers or
classNameto simplify updates.