Browse the docs

Getting started

Theming

All design decisions live in one CSS file of tokens. Components read the tokens, so changing a value restyles everything in both frameworks.

How it works

  1. 1:root holds the light theme, .dark the dark theme. Add the dark class to <html> to switch.
  2. 2Tailwind maps each token to utilities: --primary becomes bg-primary, text-primary and border-primary.
  3. 3Derived shades are computed with color-mix, so a new primary updates hover, edge and glow automatically.
  4. 4The type scale and fonts live in @theme inline, which creates text-display through text-caption.
  5. 5In the Stardust repo, edit theme/stardust.css and run bun run registry to publish the change.

Change the primary color

:root {
  --primary: oklch(0.38 0.152 18.6);   /* #800020 */
  --primary-highlight: oklch(0.8 0.12 19.4 / 50%);
}
.dark {
  --primary: oklch(0.519 0.203 19.5);  /* #C20E35 */
}

Add a badge color

Each colored badge sets one variable; the shared recipe derives border, tint, text and icon color from it.

info: `[--badge:var(--chart-5)] ${TINT}`,

The full theme file

/* Stardust tokens. Single source for the React and Vue registries; edit here, then run `bun run registry`. */

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

  /* Load Geist in the app (next/font or a <link>); the stack falls back to the system font. */
  --font-sans: "Geist", ui-sans-serif, system-ui, sans-serif;
  --font-heading: "Geist", ui-sans-serif, system-ui, sans-serif;

  /* Type scale: text-display, text-title, text-heading, text-body, text-caption. */
  --text-display: 2.25rem;
  --text-display--line-height: 2.75rem;
  --text-display--letter-spacing: -0.025em;
  --text-display--font-weight: 600;
  --text-title: 1.5rem;
  --text-title--line-height: 2rem;
  --text-title--letter-spacing: -0.02em;
  --text-title--font-weight: 600;
  --text-heading: 1.0625rem;
  --text-heading--line-height: 1.5rem;
  --text-heading--letter-spacing: -0.01em;
  --text-heading--font-weight: 600;
  --text-body: 0.875rem;
  --text-body--line-height: 1.5rem;
  --text-caption: 0.75rem;
  --text-caption--line-height: 1rem;
  --text-caption--letter-spacing: 0.01em;
}

/* Light: rose-tinted neutrals, burgundy primary (#800020). White on primary is 10.8:1.
   Palette: burgundy #800020 (brightened from #621C36), crimson #DA2049, salmon #FB7A7F, coral #F25151. */
:root {
  --radius: 0.75rem;
  --background: oklch(0.995 0.002 15);
  --foreground: oklch(0.2 0.012 15);
  --card: oklch(1 0 15);
  --card-foreground: oklch(0.2 0.012 15);
  --popover: oklch(1 0 15);
  --popover-foreground: oklch(0.2 0.012 15);
  --primary: oklch(0.38 0.152 18.6);
  --primary-foreground: oklch(1 0 15);
  /* Top-edge highlight on primary surfaces (button inner shadow): the palette's salmon, a step lighter. */
  --primary-highlight: oklch(0.8 0.12 19.4 / 50%);
  --secondary: oklch(0.965 0.008 15);
  --secondary-foreground: oklch(0.25 0.015 15);
  --muted: oklch(0.965 0.008 15);
  --muted-foreground: oklch(0.5 0.015 15);
  --accent: oklch(0.96 0.03 18);
  --accent-foreground: oklch(0.347 0.104 1.4);
  --destructive: oklch(0.55 0.22 27);
  --success: oklch(0.55 0.14 155);
  --warning: oklch(0.62 0.15 70);
  --border: oklch(0.915 0.008 15);
  --input: oklch(0.89 0.01 15);
  --ring: oklch(0.38 0.152 18.6);
  --chart-1: oklch(0.38 0.152 18.6);
  --chart-2: oklch(0.73 0.158 19.4);
  --chart-3: oklch(0.575 0.215 16.9);
  --chart-4: oklch(0.657 0.197 24.4);
  --chart-5: oklch(0.6 0.1 200);
  --sidebar: oklch(0.98 0.004 15);
  --sidebar-foreground: oklch(0.2 0.012 15);
  --sidebar-primary: oklch(0.38 0.152 18.6);
  --sidebar-primary-foreground: oklch(1 0 15);
  --sidebar-accent: oklch(0.955 0.012 15);
  --sidebar-accent-foreground: oklch(0.25 0.015 15);
  --sidebar-border: oklch(0.915 0.008 15);
  --sidebar-ring: oklch(0.38 0.152 18.6);
}

/* Dark: neutral surfaces on #0A0A0A (sidebar #0F0F0F) with a crimson-burgundy primary (#C20E35); white on it is 6.2:1.
   The focus ring uses a lighter red so it clears 3:1 against the background. */
.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.96 0 0);
  --card: oklch(0.2 0 0);
  --card-foreground: oklch(0.96 0 0);
  --popover: oklch(0.215 0 0);
  --popover-foreground: oklch(0.96 0 0);
  --primary: oklch(0.519 0.203 19.5);
  --primary-foreground: oklch(1 0 0);
  --primary-highlight: oklch(0.8 0.12 19.4 / 50%);
  --secondary: oklch(0.255 0 0);
  --secondary-foreground: oklch(0.96 0 0);
  --muted: oklch(0.255 0 0);
  --muted-foreground: oklch(0.7 0 0);
  --accent: oklch(0.29 0.085 4);
  --accent-foreground: oklch(0.84 0.1 19);
  --destructive: oklch(0.7 0.19 22);
  --success: oklch(0.74 0.15 155);
  --warning: oklch(0.8 0.15 75);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 14%);
  --ring: oklch(0.6 0.2 19.5);
  --chart-1: oklch(0.519 0.203 19.5);
  --chart-2: oklch(0.73 0.158 19.4);
  --chart-3: oklch(0.52 0.17 2);
  --chart-4: oklch(0.657 0.197 24.4);
  --chart-5: oklch(0.7 0.1 200);
  --sidebar: oklch(0.17 0 0);
  --sidebar-foreground: oklch(0.96 0 0);
  --sidebar-primary: oklch(0.519 0.203 19.5);
  --sidebar-primary-foreground: oklch(1 0 0);
  --sidebar-accent: oklch(0.255 0 0);
  --sidebar-accent-foreground: oklch(0.96 0 0);
  --sidebar-border: oklch(1 0 0 / 10%);
  --sidebar-ring: oklch(0.6 0.2 19.5);
}

/* Tailwind v4 dropped the pointer cursor on buttons; Stardust restores it for every enabled button.
   Radix switches, checkboxes, tabs and select triggers render as <button>, so they get it too. */
@layer base {
  button:not(:disabled), [role="button"]:not([aria-disabled="true"]) {
    cursor: pointer;
  }
}