UNPKG

@loke/design-system

Version:

A design system with individually importable components

355 lines (279 loc) 13.1 kB
--- name: theming description: > Customize design system appearance via CSS custom properties and Tailwind utilities. Brand color scale --brand-50 through --brand-950 (oklch). Semantic tokens: --primary, --secondary, --accent, --destructive, --muted, --background, --foreground, --card, --popover, --border, --input, --ring. Sidebar tokens, chart tokens. Dark mode via .dark class (not prefers-color-scheme). Border radius derived from --radius base. cn() utility (clsx + tailwind-merge). CVA for variant definitions. Override via className props using semantic token classes (bg-primary, text-muted-foreground). Activate when customizing theme, colors, dark mode, or component appearance. type: core library: '@loke/design-system' library_version: '2.0.0-rc.6' requires: - getting-started sources: - 'LOKE/merchant-frontends:packages/design-system/src/styles/index.css' - 'LOKE/merchant-frontends:packages/design-system/src/styles/theme.css' - 'LOKE/merchant-frontends:packages/design-system/src/lib/cn' - 'LOKE/merchant-frontends:apps/office/src/styles.css' --- ## Dependency This skill builds on getting-started. Read it first for CSS configuration. ## Setup The design system uses CSS custom properties (tokens) defined on `:root` for light mode and overridden on `.dark` for dark mode. Tailwind's `@theme` block maps these tokens to Tailwind utility classes, so you write `bg-primary` or `text-muted-foreground` instead of raw color values. ```css /* Light mode (default) */ :root { --primary: oklch(51.4% 0.2276 276.98); --primary-foreground: oklch(99.17% 0.0028 325.6); --background: var(--color-brand-50); --foreground: var(--color-brand-950); /* ... */ } /* Dark mode override */ .dark { --primary: oklch(60% 0.25 276.98); --primary-foreground: oklch(15% 0.01 325.6); --background: oklch(15% 0.02 274.82); --foreground: var(--color-brand-50); /* ... */ } ``` In components, use the Tailwind utility classes that map to these tokens: ```tsx <div className="bg-background text-foreground"> <h1 className="text-primary">Heading</h1> <p className="text-muted-foreground">Subdued text</p> </div> ``` ## Core Patterns ### Semantic Token System All semantic tokens defined in the design system CSS. Each has a light (`:root`) and dark (`.dark`) value. The `@theme` block maps `--token` to `--color-token`, enabling Tailwind classes like `bg-primary`, `text-card-foreground`, etc. | Token | Tailwind Class (bg/text) | Light Value | Dark Value | |---|---|---|---| | `--background` | `bg-background` | `var(--color-brand-50)` | `oklch(15% 0.02 274.82)` | | `--foreground` | `text-foreground` | `var(--color-brand-950)` | `var(--color-brand-50)` | | `--primary` | `bg-primary`, `text-primary` | `var(--color-brand-500)` | `oklch(60% 0.25 276.98)` | | `--primary-foreground` | `text-primary-foreground` | `oklch(99.17% 0.0028 325.6)` | `oklch(15% 0.01 325.6)` | | `--secondary` | `bg-secondary` | `oklch(72.29% 0.1438 163.11)` | `oklch(55% 0.16 163.11)` | | `--secondary-foreground` | `text-secondary-foreground` | `oklch(99.78% 0.0068 115.7)` | `oklch(15% 0.02 115.7)` | | `--accent` | `bg-accent` | `oklch(96.71% 0.0029 264.54)` | `oklch(30% 0.05 264.54)` | | `--accent-foreground` | `text-accent-foreground` | `oklch(21.03% 0.0318 264.65)` | `oklch(95% 0.04 264.65)` | | `--destructive` | `bg-destructive` | `oklch(58.52% 0.18 24.61)` | `oklch(60% 0.22 24.61)` | | `--destructive-foreground` | `text-destructive-foreground` | `oklch(98.48% 0.0213 193.18)` | `oklch(15% 0.02 193.18)` | | `--muted` | `bg-muted` | `oklch(96.71% 0.0029 264.54)` | `oklch(25% 0.01 264.54)` | | `--muted-foreground` | `text-muted-foreground` | `oklch(55.13% 0.0233 264.36)` | `oklch(75% 0.03 264.36)` | | `--card` | `bg-card` | `oklch(99.16% 0.0029 247.85)` | `oklch(20% 0.01 247.85)` | | `--card-foreground` | `text-card-foreground` | `oklch(37.39% 0.0822 285.49)` | `oklch(85% 0.04 283.75)` | | `--popover` | `bg-popover` | `oklch(99.16% 0.0029 247.85)` | `oklch(18% 0.015 247.85)` | | `--popover-foreground` | `text-popover-foreground` | `oklch(37.1% 0.0722 285.35)` | `oklch(92% 0.07 285.35)` | | `--border` | `border-border` | `oklch(90.58% 0.013831 272.4947)` | `oklch(30% 0.02 272.49)` | | `--input` | `border-input` | `oklch(87.53% 0.0142 268.67)` | `oklch(28% 0.02 268.67)` | | `--ring` | `ring-ring` | `oklch(74.83% 0.1308 254.23)` | `oklch(65% 0.18 254.23)` | The base layer applies defaults globally: ```css @layer base { * { @apply border-border outline-ring/50; } body { @apply bg-background text-foreground; } } ``` ### Brand Color Scale The brand scale defines `--brand-50` through `--brand-950` in oklch. These feed into `--primary`, `--background`, and `--foreground` via `var()` references. | Token | Value | |---|---| | `--brand-50` | `oklch(96% 0.04 276.98)` | | `--brand-100` | `oklch(92% 0.08 276.98)` | | `--brand-200` | `oklch(83% 0.13 276.98)` | | `--brand-300` | `oklch(74% 0.18 276.98)` | | `--brand-400` | `oklch(63% 0.21 276.98)` | | `--brand-500` | `oklch(51.4% 0.2276 276.98)` | | `--brand-600` | `oklch(42% 0.2 276.98)` | | `--brand-700` | `oklch(33% 0.17 276.98)` | | `--brand-800` | `oklch(24% 0.13 276.98)` | | `--brand-900` | `oklch(15% 0.09 276.98)` | | `--brand-950` | `oklch(5% 0.05 276.98)` | All brand tokens share the same hue (`276.98`). The `@theme` block maps these to `--color-brand-*`, enabling Tailwind classes like `bg-brand-500` or `text-brand-200`. To customize the brand color, override the full scale in your consumer CSS `:root` block: ```css :root { --brand-50: oklch(96% 0.03 150); --brand-100: oklch(92% 0.06 150); /* ... override all 11 steps ... */ --brand-950: oklch(5% 0.04 150); } ``` ### Dark Mode Dark mode is activated by adding the `.dark` class to the `<html>` or `<body>` element. The design system uses a custom variant: ```css @custom-variant dark (&:is(.dark *)); ``` This means `dark:` utilities in Tailwind only activate when an ancestor has the `.dark` class -- not via `prefers-color-scheme`. Semantic tokens auto-switch between light and dark values, so most components need no `dark:` prefix at all. ```tsx // Toggle dark mode document.documentElement.classList.toggle("dark"); ``` ### cn() Utility `cn()` combines `clsx` (conditional class joining) with `twMerge` (Tailwind class deduplication). Import from `@loke/design-system/cn`. ```ts import { cn } from "@loke/design-system/cn"; // Conditional classes cn("px-4 py-2", isActive && "bg-primary text-primary-foreground"); // Override base classes -- twMerge resolves conflicts cn("bg-muted text-sm", className); // className="bg-primary" wins over bg-muted ``` Source implementation: ```ts import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } ``` ### CVA for Component Variants Components use `class-variance-authority` (CVA) to define variant-driven class sets. Here is the Button component as a reference: ```tsx import { cn } from "@loke/design-system/cn"; import { cva, type VariantProps } from "class-variance-authority"; export const buttonVariants = cva( cn( "inline-flex items-center justify-center shrink-0 whitespace-nowrap", "rounded-lg border border-transparent text-sm font-medium", "disabled:pointer-events-none disabled:opacity-50", ), { defaultVariants: { size: "default", variant: "default", }, variants: { size: { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "size-8", }, variant: { default: "bg-primary text-primary-foreground hover:bg-brand-900 dark:hover:bg-brand-100", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/60", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, }, }, ); ``` Variants reference semantic tokens (`bg-primary`, `text-destructive-foreground`) so they auto-switch in dark mode. The `className` prop is passed through `cn()` to allow per-instance overrides: ```tsx <Button variant="outline" className="bg-muted"> Custom background </Button> ``` ### Overriding Component Appearance All components accept a `className` prop merged via `cn()`. Use semantic token classes for overrides so dark mode continues to work: ```tsx <Card className="bg-muted" /> <Button className="bg-brand-500 hover:bg-brand-600" /> <Badge className="bg-accent text-accent-foreground" /> ``` ### Chart Tokens Five chart tokens for data visualization (e.g., recharts): | Token | Light Value | Dark Value | |---|---|---| | `--chart-1` | `var(--color-brand-300)` | `oklch(0.70 0.13 162)` | | `--chart-2` | `oklch(85.59% 0.0601 285.82)` | `oklch(0.68 0.12 198)` | | `--chart-3` | `oklch(72.29% 0.1438 163.11)` | `oklch(0.68 0.14 53)` | | `--chart-4` | `oklch(75.5% 0.145 230.45)` | `oklch(0.66 0.18 285)` | | `--chart-5` | `oklch(68.75% 0.154 38.2)` | `oklch(0.65 0.19 16)` | Use in Tailwind: `bg-chart-1`, `text-chart-3`, `stroke-chart-2`, etc. ### Sidebar Tokens Dedicated tokens for sidebar theming. The sidebar uses a dark background even in light mode by default: | Token | Light Value | Dark Value | |---|---|---| | `--sidebar` | `oklch(21.03% 0.0316 264.78)` | `oklch(20% 0.01 247.85)` | | `--sidebar-foreground` | `oklch(95.05% 0.0041 286.32)` | `oklch(97% 0.01 286.32)` | | `--sidebar-accent` | `oklch(28.01% 0.0249 258.35)` | `oklch(28.01% 0.0249 258.35)` | | `--sidebar-accent-foreground` | `oklch(72.47% 0.1495 160.94)` | `oklch(55% 0.16 163.11)` | | `--sidebar-border` | `oklch(28.01% 0.0249 258.35)` | `oklch(28.01% 0.0249 258.35)` | | `--sidebar-ring` | `oklch(74.83% 0.1308 254.23)` | `oklch(65% 0.18 254.23)` | Tailwind classes: `bg-sidebar`, `text-sidebar-foreground`, `border-sidebar-border`, etc. ### Border Radius Border radius derives from a single `--radius` base (default `0.5rem`): ```css --radius-sm: calc(var(--radius) - 4px); /* 0.125rem at default */ --radius-md: calc(var(--radius) - 2px); /* 0.375rem at default */ --radius-lg: var(--radius); /* 0.5rem at default */ --radius-xl: calc(var(--radius) + 4px); /* 0.75rem at default */ ``` Override `--radius` in your consumer CSS to scale all radii uniformly: ```css :root { --radius: 0.75rem; /* larger, more rounded */ } ``` ## Common Mistakes ### CRITICAL: Using raw color values instead of semantic tokens ```tsx // Wrong -- breaks in dark mode, ignores theme <div className="bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-100"> // Correct -- auto-switches with theme <div className="bg-muted text-muted-foreground"> ``` Semantic tokens handle light/dark switching automatically. Raw Tailwind color classes (`gray-*`, `blue-*`) bypass the theme system entirely. ### HIGH: Overriding brand colors in Tailwind config instead of CSS ```js // Wrong -- do not add colors to tailwind.config.js or @theme module.exports = { theme: { extend: { colors: { brand: { 500: "#5B21B6" }, }, }, }, }; // Correct -- override CSS custom properties in your consumer CSS // (e.g., apps/office/src/styles.css) :root { --brand-50: oklch(96% 0.03 270); --brand-500: oklch(51% 0.23 270); /* ... all 11 steps ... */ } ``` The `@theme` block in `index.css` already maps `--brand-*` variables to `--color-brand-*`. Override the CSS variables, not the Tailwind config. ### HIGH: Using dark: media prefix instead of .dark class ```tsx // Wrong -- media-based dark mode, will not match DS behavior <div className="dark:bg-gray-800"> // Correct -- use semantic tokens that auto-switch <div className="bg-background"> // If you must target dark mode explicitly, the .dark class variant works: <div className="dark:bg-brand-900"> // This works because the DS defines: @custom-variant dark (&:is(.dark *)); ``` The design system uses class-based dark mode (`@custom-variant dark (&:is(.dark *))`), not `prefers-color-scheme`. Semantic tokens are the preferred path since they auto-switch without any `dark:` prefix. ### HIGH: Not importing theme CSS ```tsx // Wrong -- components render unstyled or with wrong colors import { Button } from "@loke/design-system/button"; // Correct -- import styles which includes theme tokens AND component styles import "@loke/design-system/styles"; import { Button } from "@loke/design-system/button"; ``` The styles import loads both the theme tokens (`:root` and `.dark` variables) and base layer styles. Without it, CSS custom properties are undefined and components fall back to browser defaults. ## See Also - getting-started/SKILL.md -- initial CSS setup and @source directive - layout/SKILL.md -- responsive props interact with Tailwind utilities (tension)