TanStack Start integration
Set up a TanStack Start project with Untitled UI React in seconds. Our TanStack Start starter kit includes providers, routing, and everything you need to get started.
Installation
The easiest way to get started with Untitled UI in a TanStack Start project is to use our CLI:
Initialize a new TanStack Start project with Untitled UI
Using our CLI, you can initialize a new TanStack Start project with Untitled UI with the following command:
npx untitledui@latest init untitled-ui --tanstack
While running the command, you'll be asked which color to use as the brand color:
? Which color would you like to use as the brand color? › - Use arrow-keys. Return to submit. ❯ brand red orange amber yellow lime green ... zinc neutral stone
This will create a new TanStack Start project in the untitled-ui directory with all the necessary configurations and components pre-installed. The choices are Tailwind's built-in palettes; picking one points the --color-brand-* variables in theme.css at it, and brand keeps the default purple. You can change the brand color at any time later (see Theming). If you leave out the directory name, the CLI asks for it first.
Once it's done, start the development server, which serves the app at http://localhost:3000:
cd untitled-ui npm run dev
Ready to go!
Great! You're all set to start using Untitled UI components.
If something is missing, you can copy/paste what you need into your project directly from individual components pages.
Need help? Check our GitHub repository for examples, or open an issue if you run into any problems. Our community is here to help!
Manual installation
If you prefer to add Untitled UI to an existing TanStack Start project, you can follow these steps:
Install packages
The TanStack Start template already includes tailwindcss and @tailwindcss/vite, so install the remaining packages with your favorite package manager:
npm install @untitledui/icons react-aria-components tailwindcss-react-aria-components tailwind-merge tailwindcss-animate
Check the Vite configuration
TanStack Start runs on Vite, and the template's vite.config.ts already has everything Untitled UI needs:
import { defineConfig } from 'vite' import { devtools } from '@tanstack/devtools-vite' import { tanstackStart } from '@tanstack/react-start/plugin/vite' import viteReact from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' const config = defineConfig({ resolve: { tsconfigPaths: true }, plugins: [devtools(), tailwindcss(), tanstackStart(), viteReact()], }) export default config
This configuration does two important things:
- Adds the Tailwind CSS Vite plugin for efficient processing
- Resolves the
@/path alias from yourtsconfig.json
If your project was set up differently, make sure both are in place.
Update TypeScript configuration
The template's tsconfig.json already maps @/* to ./src/*. Change lib from ES2022 to ESNext, because some components use newer JavaScript methods such as Array.prototype.toReversed():
{ "compilerOptions": { // other options... "lib": ["ESNext", "DOM", "DOM.Iterable"], "paths": { "#/*": ["./src/*"], "@/*": ["./src/*"] } } }
Create a custom Tailwind CSS configuration
Create a theme.css file in src/styles and add the following code:
@theme { /* FONT FAMILY */ --font-body: var(--font-inter, "Inter"), -apple-system, "Segoe UI", Roboto, Arial, sans-serif; --font-display: var(--font-inter, "Inter"), -apple-system, "Segoe UI", Roboto, Arial, sans-serif; --font-mono: ui-monospace, "Roboto Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* FONT SIZE */ --text-xs: calc(var(--spacing) * 3); --text-xs--line-height: calc(var(--spacing) * 4.5); --text-sm: calc(var(--spacing) * 3.5); --text-sm--line-height: calc(var(--spacing) * 5); --text-md: calc(var(--spacing) * 4); --text-md--line-height: calc(var(--spacing) * 6); --text-lg: calc(var(--spacing) * 4.5); --text-lg--line-height: calc(var(--spacing) * 7); --text-xl: calc(var(--spacing) * 5); --text-xl--line-height: calc(var(--spacing) * 7.5); --text-display-xs: calc(var(--spacing) * 6); --text-display-xs--line-height: calc(var(--spacing) * 8); --text-display-sm: calc(var(--spacing) * 7.5); --text-display-sm--line-height: calc(var(--spacing) * 9.5); --text-display-md: calc(var(--spacing) * 9); --text-display-md--line-height: calc(var(--spacing) * 11); --text-display-md--letter-spacing: -0.72px; --text-display-lg: calc(var(--spacing) * 12); --text-display-lg--line-height: calc(var(--spacing) * 15); --text-display-lg--letter-spacing: -0.96px; --text-display-xl: calc(var(--spacing) * 15); --text-display-xl--line-height: calc(var(--spacing) * 18); --text-display-xl--letter-spacing: -1.2px; --text-display-2xl: calc(var(--spacing) * 18); --text-display-2xl--line-height: calc(var(--spacing) * 22.5); --text-display-2xl--letter-spacing: -1.44px; /* MAX WIDTH */ --max-width-container: 1280px; /* BREAKPOINTS */ --breakpoint-xxs: 320px; /* This must match the breakpoint in Sonner: https://github.com/emilkowalski/sonner/blob/main/src/styles.css */ --breakpoint-xs: 600px; /* RADIUS */ --radius-none: 0px; --radius-xs: 0.125rem; --radius-sm: 0.25rem; --radius-DEFAULT: 0.25rem; --radius-md: 0.375rem; --radius-lg: 0.5rem; --radius-xl: 0.75rem; --radius-2xl: 1rem; --radius-3xl: 1.5rem; --radius-full: 9999px; /* SHADOW */ --shadow-xs: 0px 1px 2px rgba(0, 0, 0, 0.05); --shadow-sm: 0px 1px 3px rgba(0, 0, 0, 0.1), 0px 1px 2px -1px rgba(0, 0, 0, 0.1); --shadow-md: 0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.06); --shadow-lg: 0px 12px 16px -4px rgba(0, 0, 0, 0.08), 0px 4px 6px -2px rgba(0, 0, 0, 0.03), 0px 2px 2px -1px rgba(0, 0, 0, 0.04); --shadow-xl: 0px 20px 24px -4px rgba(0, 0, 0, 0.08), 0px 8px 8px -4px rgba(0, 0, 0, 0.03), 0px 3px 3px -1.5px rgba(0, 0, 0, 0.04); --shadow-2xl: 0px 24px 48px -12px rgba(0, 0, 0, 0.18), 0px 4px 4px -2px rgba(0, 0, 0, 0.04); --shadow-3xl: 0px 32px 64px -12px rgba(0, 0, 0, 0.14), 0px 5px 5px -2.5px rgba(0, 0, 0, 0.04); --shadow-skeuomorphic: 0px 0px 0px 1px rgba(0, 0, 0, 0.18) inset, 0px -2px 0px 0px rgba(0, 0, 0, 0.05) inset; --shadow-xs-skeuomorphic: var(--shadow-skeuomorphic), var(--shadow-xs); --shadow-modern-mockup-inner-lg: 0px 0px 3.765px 1.255px rgba(10, 13, 18, 0.08) inset, 0px 0px 2.51px 1.255px rgba(10, 13, 18, 0.03) inset; --shadow-modern-mockup-inner-md: 0px 0px 1.692px 0.564px rgba(10, 13, 18, 0.08) inset, 0px 0px 1.128px 0.564px rgba(10, 13, 18, 0.03) inset; --shadow-modern-mockup-inner-sm: 0px 0px 4.48px 1.493px rgba(10, 13, 18, 0.08) inset, 0px 0px 2.987px 1.493px rgba(10, 13, 18, 0.03) inset; --shadow-modern-mockup-outer-lg: 0px 7.529px 10.039px -2.51px rgba(10, 13, 18, 0.08), 0px 2.51px 3.765px -1.255px rgba(10, 13, 18, 0.03), 0px 1.255px 1.255px -0.627px rgba(10, 13, 18, 0.04); --shadow-modern-mockup-outer-md: 0px 3.385px 4.513px -1.128px rgba(10, 13, 18, 0.08), 0px 1.128px 1.692px -0.564px rgba(10, 13, 18, 0.03), 0px 0.564px 0.564px -0.282px rgba(10, 13, 18, 0.04); --drop-shadow-iphone-mockup: 20px 12px 18px rgba(16, 24, 40, 0.2); /* ANIMATIONS */ --animate-marquee: marquee 60s linear infinite; --animate-caret-blink: caret-blink 1s infinite; @keyframes marquee { 0% { transform: translateX(0); } 100% { transform: translateX(-100%); } } @keyframes caret-blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } } /* BASE COLOR */ --color-transparent: rgb(0 0 0 / 0); --color-white: rgb(255 255 255); --color-black: rgb(0 0 0); /* These will be inverted in dark mode. */ --color-alpha-white: rgb(255 255 255); --color-alpha-black: rgb(0 0 0); --color-brand-50: rgb(249 245 255); --color-brand-100: rgb(244 235 255); --color-brand-200: rgb(233 215 254); --color-brand-300: rgb(214 187 251); --color-brand-400: rgb(182 146 246); --color-brand-500: rgb(158 119 237); --color-brand-600: rgb(127 86 217); --color-brand-700: rgb(105 65 198); --color-brand-800: rgb(83 56 158); --color-brand-900: rgb(66 48 125); --color-brand-950: rgb(44 28 95); /* LIGHT MODE VARIABLES */ --color-alpha-white: rgb(255 255 255); --color-alpha-black: rgb(0 0 0); /* UTILITY COLORS */ --color-utility-blue-50: var(--color-blue-50); --color-utility-blue-100: var(--color-blue-100); --color-utility-blue-200: var(--color-blue-200); --color-utility-blue-300: var(--color-blue-300); --color-utility-blue-400: var(--color-blue-400); --color-utility-blue-500: var(--color-blue-500); --color-utility-blue-600: var(--color-blue-600); --color-utility-blue-700: var(--color-blue-700); --color-utility-brand-50: var(--color-brand-50); --color-utility-brand-50_alt: var(--color-brand-50); --color-utility-brand-100: var(--color-brand-100); --color-utility-brand-100_alt: var(--color-brand-100); --color-utility-brand-200: var(--color-brand-200); --color-utility-brand-200_alt: var(--color-brand-200); --color-utility-brand-300: var(--color-brand-300); --color-utility-brand-300_alt: var(--color-brand-300); --color-utility-brand-400: var(--color-brand-400); --color-utility-brand-400_alt: var(--color-brand-400); --color-utility-brand-500: var(--color-brand-500); --color-utility-brand-500_alt: var(--color-brand-500); --color-utility-brand-600: var(--color-brand-600); --color-utility-brand-600_alt: var(--color-brand-600); --color-utility-brand-700: var(--color-brand-700); --color-utility-brand-700_alt: var(--color-brand-700); --color-utility-brand-800: var(--color-brand-800); --color-utility-brand-800_alt: var(--color-brand-800); --color-utility-brand-900: var(--color-brand-900); --color-utility-brand-900_alt: var(--color-brand-900); --color-utility-neutral-50: var(--color-neutral-50); --color-utility-neutral-100: var(--color-neutral-100); --color-utility-neutral-200: var(--color-neutral-200); --color-utility-neutral-300: var(--color-neutral-300); --color-utility-neutral-400: var(--color-neutral-400); --color-utility-neutral-500: var(--color-neutral-500); --color-utility-neutral-600: var(--color-neutral-600); --color-utility-neutral-700: var(--color-neutral-700); --color-utility-neutral-800: var(--color-neutral-800); --color-utility-neutral-900: var(--color-neutral-900); --color-utility-red-50: var(--color-red-50); --color-utility-red-100: var(--color-red-100); --color-utility-red-200: var(--color-red-200); --color-utility-red-300: var(--color-red-300); --color-utility-red-400: var(--color-red-400); --color-utility-red-500: var(--color-red-500); --color-utility-red-600: var(--color-red-600); --color-utility-red-700: var(--color-red-700); --color-utility-yellow-50: var(--color-yellow-50); --color-utility-yellow-100: var(--color-yellow-100); --color-utility-yellow-200: var(--color-yellow-200); --color-utility-yellow-300: var(--color-yellow-300); --color-utility-yellow-400: var(--color-yellow-400); --color-utility-yellow-500: var(--color-yellow-500); --color-utility-yellow-600: var(--color-yellow-600); --color-utility-yellow-700: var(--color-yellow-700); --color-utility-green-50: var(--color-green-50); --color-utility-green-100: var(--color-green-100); --color-utility-green-200: var(--color-green-200); --color-utility-green-300: var(--color-green-300); --color-utility-green-400: var(--color-green-400); --color-utility-green-500: var(--color-green-500); --color-utility-green-600: var(--color-green-600); --color-utility-green-700: var(--color-green-700); --color-utility-orange-50: var(--color-orange-50); --color-utility-orange-100: var(--color-orange-100); --color-utility-orange-200: var(--color-orange-200); --color-utility-orange-300: var(--color-orange-300); --color-utility-orange-400: var(--color-orange-400); --color-utility-orange-500: var(--color-orange-500); --color-utility-orange-600: var(--color-orange-600); --color-utility-orange-700: var(--color-orange-700); --color-utility-indigo-50: var(--color-indigo-50); --color-utility-indigo-100: var(--color-indigo-100); --color-utility-indigo-200: var(--color-indigo-200); --color-utility-indigo-300: var(--color-indigo-300); --color-utility-indigo-400: var(--color-indigo-400); --color-utility-indigo-500: var(--color-indigo-500); --color-utility-indigo-600: var(--color-indigo-600); --color-utility-indigo-700: var(--color-indigo-700); --color-utility-fuchsia-50: var(--color-fuchsia-50); --color-utility-fuchsia-100: var(--color-fuchsia-100); --color-utility-fuchsia-200: var(--color-fuchsia-200); --color-utility-fuchsia-300: var(--color-fuchsia-300); --color-utility-fuchsia-400: var(--color-fuchsia-400); --color-utility-fuchsia-500: var(--color-fuchsia-500); --color-utility-fuchsia-600: var(--color-fuchsia-600); --color-utility-fuchsia-700: var(--color-fuchsia-700); --color-utility-pink-50: var(--color-pink-50); --color-utility-pink-100: var(--color-pink-100); --color-utility-pink-200: var(--color-pink-200); --color-utility-pink-300: var(--color-pink-300); --color-utility-pink-400: var(--color-pink-400); --color-utility-pink-500: var(--color-pink-500); --color-utility-pink-600: var(--color-pink-600); --color-utility-pink-700: var(--color-pink-700); --color-utility-purple-50: var(--color-purple-50); --color-utility-purple-100: var(--color-purple-100); --color-utility-purple-200: var(--color-purple-200); --color-utility-purple-300: var(--color-purple-300); --color-utility-purple-400: var(--color-purple-400); --color-utility-purple-500: var(--color-purple-500); --color-utility-purple-600: var(--color-purple-600); --color-utility-purple-700: var(--color-purple-700); --color-utility-sky-50: var(--color-sky-50); --color-utility-sky-100: var(--color-sky-100); --color-utility-sky-200: var(--color-sky-200); --color-utility-sky-300: var(--color-sky-300); --color-utility-sky-400: var(--color-sky-400); --color-utility-sky-500: var(--color-sky-500); --color-utility-sky-600: var(--color-sky-600); --color-utility-sky-700: var(--color-sky-700); --color-utility-slate-50: var(--color-slate-50); --color-utility-slate-100: var(--color-slate-100); --color-utility-slate-200: var(--color-slate-200); --color-utility-slate-300: var(--color-slate-300); --color-utility-slate-400: var(--color-slate-400); --color-utility-slate-500: var(--color-slate-500); --color-utility-slate-600: var(--color-slate-600); --color-utility-slate-700: var(--color-slate-700); --color-utility-emerald-50: var(--color-emerald-50); --color-utility-emerald-100: var(--color-emerald-100); --color-utility-emerald-200: var(--color-emerald-200); --color-utility-emerald-300: var(--color-emerald-300); --color-utility-emerald-400: var(--color-emerald-400); --color-utility-emerald-500: var(--color-emerald-500); --color-utility-emerald-600: var(--color-emerald-600); --color-utility-emerald-700: var(--color-emerald-700); --color-utility-amber-50: var(--color-amber-50); --color-utility-amber-100: var(--color-amber-100); --color-utility-amber-200: var(--color-amber-200); --color-utility-amber-300: var(--color-amber-300); --color-utility-amber-400: var(--color-amber-400); --color-utility-amber-500: var(--color-amber-500); --color-utility-amber-600: var(--color-amber-600); --color-utility-amber-700: var(--color-amber-700); /* TEXT COLORS */ --color-text-primary: var(--color-neutral-900); --color-text-tertiary: var(--color-neutral-600); --color-text-error-primary: var(--color-red-600); --color-text-warning-primary: var(--color-yellow-600); --color-text-success-primary: var(--color-green-600); --color-text-white: var(--color-white); --color-text-secondary: var(--color-neutral-700); --color-text-secondary_hover: var(--color-neutral-800); --color-text-tertiary_hover: var(--color-neutral-700); --color-text-brand-secondary: var(--color-brand-700); --color-text-placeholder: var(--color-neutral-500); --color-text-brand-tertiary: var(--color-brand-600); --color-text-editor-icon-fg: var(--color-neutral-400); --color-text-editor-icon-fg_active: var(--color-neutral-500); --color-text-quaternary: var(--color-neutral-500); --color-text-brand-primary: var(--color-brand-900); --color-text-primary_on-brand: var(--color-white); --color-text-secondary_on-brand: var(--color-brand-200); --color-text-tertiary_on-brand: var(--color-brand-200); --color-text-quaternary_on-brand: var(--color-brand-300); --color-text-brand-tertiary_alt: var(--color-brand-600); --color-text-error-primary_hover: var(--color-red-700); --color-text-brand-secondary_hover: var(--color-brand-800); /* BORDER COLORS */ --color-border-primary: var(--color-neutral-300); --color-border-secondary: var(--color-neutral-200); --color-border-secondary_alt: rgb(0 0 0 / 0.1); --color-border-tertiary: var(--color-neutral-100); --color-border-error: var(--color-red-500); --color-border-error_subtle: var(--color-red-300); --color-border-brand: var(--color-brand-500); --color-border-brand_alt: var(--color-brand-600); /* FOREGROUND COLORS */ --color-fg-secondary: var(--color-neutral-700); --color-fg-warning-primary: var(--color-yellow-600); --color-fg-success-primary: var(--color-green-600); --color-fg-white: var(--color-white); --color-fg-success-secondary: var(--color-green-500); --color-fg-secondary_hover: var(--color-neutral-800); --color-fg-primary: var(--color-neutral-900); --color-fg-brand-secondary: var(--color-brand-500); --color-fg-brand-primary: var(--color-brand-600); --color-fg-quaternary: var(--color-neutral-400); --color-fg-quaternary_hover: var(--color-neutral-500); --color-fg-error-primary: var(--color-red-600); --color-fg-warning-secondary: var(--color-yellow-500); --color-fg-error-secondary: var(--color-red-500); --color-fg-tertiary: var(--color-neutral-600); --color-fg-tertiary_hover: var(--color-neutral-700); --color-fg-brand-primary_alt: var(--color-fg-brand-primary); --color-fg-brand-secondary_alt: var(--color-fg-brand-secondary); --color-fg-brand-secondary_hover: var(--color-brand-600); /* BACKGROUND COLORS */ --color-bg-primary: var(--color-white); --color-bg-tertiary: var(--color-neutral-100); --color-bg-brand-primary: var(--color-brand-50); --color-bg-error-secondary: var(--color-red-100); --color-bg-warning-primary: var(--color-yellow-50); --color-bg-warning-secondary: var(--color-yellow-100); --color-bg-success-primary: var(--color-green-50); --color-bg-success-secondary: var(--color-green-100); --color-bg-brand-solid: var(--color-brand-600); --color-bg-secondary-solid: var(--color-neutral-600); --color-bg-error-solid: var(--color-red-600); --color-bg-warning-solid: var(--color-yellow-600); --color-bg-success-solid: var(--color-green-600); --color-bg-secondary_hover: var(--color-neutral-100); --color-bg-primary_hover: var(--color-neutral-50); --color-bg-active: var(--color-neutral-50); --color-bg-brand-solid_hover: var(--color-brand-700); --color-bg-error-primary: var(--color-red-50); --color-bg-brand-secondary: var(--color-brand-100); --color-bg-secondary: var(--color-neutral-50); --color-bg-quaternary: var(--color-neutral-200); --color-bg-primary_alt: var(--color-white); --color-bg-brand-primary_alt: var(--color-brand-50); --color-bg-secondary_alt: var(--color-neutral-50); --color-bg-overlay: var(--color-neutral-950); --color-bg-brand-section: var(--color-brand-800); --color-bg-brand-section_subtle: var(--color-brand-700); --color-bg-primary-solid: var(--color-neutral-950); --color-bg-error-solid_hover: var(--color-red-700); /* COMPONENT COLORS */ --color-app-store-badge-border: 166 166 166 1; --color-avatar-styles-bg-neutral: 224 224 224 1; --color-featured-icon-light-fg-brand: var(--color-brand-600); --color-featured-icon-light-fg-error: var(--color-red-600); --color-featured-icon-light-fg-gray: var(--color-neutral-500); --color-featured-icon-light-fg-success: var(--color-green-600); --color-featured-icon-light-fg-warning: var(--color-yellow-600); --color-focus-ring-error: var(--color-red-500); --color-focus-ring: var(--color-brand-500); --color-footer-button-fg: var(--color-brand-200); --color-footer-button-fg_hover: var(--color-white); --color-icon-fg-brand: var(--color-brand-600); --color-icon-fg-brand_on-brand: var(--color-brand-200); --color-screen-mockup-border: var(--color-neutral-900); --color-slider-handle-bg: var(--color-white); --color-slider-handle-border: var(--color-brand-600); --color-toggle-border: var(--color-neutral-300); --color-toggle-slim-border_pressed-hover: var(--color-bg-brand-solid_hover); --color-toggle-slim-border_pressed: var(--color-bg-brand-solid); --color-tooltip-supporting-text: var(--color-neutral-300); /* BACKGROUND PROPERTY COLORS */ --background-color-primary: var(--color-bg-primary); --background-color-tertiary: var(--color-bg-tertiary); --background-color-brand-primary: var(--color-bg-brand-primary); --background-color-error-secondary: var(--color-bg-error-secondary); --background-color-warning-primary: var(--color-bg-warning-primary); --background-color-warning-secondary: var(--color-bg-warning-secondary); --background-color-success-primary: var(--color-bg-success-primary); --background-color-success-secondary: var(--color-bg-success-secondary); --background-color-brand-solid: var(--color-bg-brand-solid); --background-color-secondary-solid: var(--color-bg-secondary-solid); --background-color-error-solid: var(--color-bg-error-solid); --background-color-warning-solid: var(--color-bg-warning-solid); --background-color-success-solid: var(--color-bg-success-solid); --background-color-secondary_hover: var(--color-bg-secondary_hover); --background-color-primary_hover: var(--color-bg-primary_hover); --background-color-active: var(--color-bg-active); --background-color-brand-solid_hover: var(--color-bg-brand-solid_hover); --background-color-error-primary: var(--color-bg-error-primary); --background-color-brand-secondary: var(--color-bg-brand-secondary); --background-color-secondary: var(--color-bg-secondary); --background-color-quaternary: var(--color-bg-quaternary); --background-color-primary_alt: var(--color-bg-primary_alt); --background-color-brand-primary_alt: var(--color-bg-brand-primary_alt); --background-color-secondary_alt: var(--color-bg-secondary_alt); --background-color-overlay: var(--color-bg-overlay); --background-color-brand-section: var(--color-bg-brand-section); --background-color-brand-section_subtle: var(--color-bg-brand-section_subtle); --background-color-primary-solid: var(--color-bg-primary-solid); --background-color-error-solid_hover: var(--color-bg-error-solid_hover); --background-color-border-brand: var(--color-border-brand); --background-color-border-brand_alt: var(--color-border-brand_alt); /* TEXT PROPERTY COLORS */ --color-text-white: var(--color-white); --text-color-primary: var(--color-text-primary); --text-color-secondary: var(--color-text-secondary); --text-color-secondary_hover: var(--color-text-secondary_hover); --text-color-tertiary: var(--color-text-tertiary); --text-color-tertiary_hover: var(--color-text-tertiary_hover); --text-color-error-primary: var(--color-text-error-primary); --text-color-warning-primary: var(--color-text-warning-primary); --text-color-success-primary: var(--color-text-success-primary); --text-color-brand-secondary: var(--color-text-brand-secondary); --text-color-placeholder: var(--color-text-placeholder); --text-color-brand-tertiary: var(--color-text-brand-tertiary); --text-color-editor-icon-fg: var(--color-text-editor-icon-fg); --text-color-editor-icon-fg_active: var(--color-text-editor-icon-fg_active); --text-color-quaternary: var(--color-text-quaternary); --text-color-brand-primary: var(--color-text-brand-primary); --text-color-primary_on-brand: var(--color-text-primary_on-brand); --text-color-secondary_on-brand: var(--color-text-secondary_on-brand); --text-color-tertiary_on-brand: var(--color-text-tertiary_on-brand); --text-color-quaternary_on-brand: var(--color-text-quaternary_on-brand); --text-color-brand-tertiary_alt: var(--color-text-brand-tertiary_alt); --text-color-error-primary_hover: var(--color-text-error-primary_hover); --text-color-brand-secondary_hover: var(--color-text-brand-secondary_hover); --text-color-tooltip-supporting-text: var(--color-tooltip-supporting-text); /* BORDER PROPERTY COLORS */ --border-color-primary: var(--color-border-primary); --border-color-secondary: var(--color-border-secondary); --border-color-secondary_alt: var(--color-border-secondary_alt); --border-color-tertiary: var(--color-border-tertiary); --border-color-error: var(--color-border-error); --border-color-error_subtle: var(--color-border-error_subtle); --border-color-brand: var(--color-border-brand); --border-color-brand_alt: var(--color-border-brand_alt); --border-color-brand-solid: var(--color-bg-brand-solid); --border-color-brand-solid_hover: var(--color-bg-brand-solid_hover); /* RING PROPERTY COLORS */ --ring-color-primary: var(--color-border-primary); --ring-color-secondary: var(--color-border-secondary); --ring-color-secondary_alt: var(--color-border-secondary_alt); --ring-color-tertiary: var(--color-border-tertiary); --ring-color-error: var(--color-border-error); --ring-color-error_subtle: var(--color-border-error_subtle); --ring-color-brand: var(--color-border-brand); --ring-color-brand_alt: var(--color-border-brand_alt); --ring-color-brand-solid: var(--color-bg-brand-solid); --ring-color-brand-solid_hover: var(--color-bg-brand-solid_hover); /* OUTLINE PROPERTY COLORS */ --outline-color-primary: var(--color-border-primary); --outline-color-secondary: var(--color-border-secondary); --outline-color-secondary_alt: var(--color-border-secondary_alt); --outline-color-tertiary: var(--color-border-tertiary); --outline-color-error: var(--color-border-error); --outline-color-error_subtle: var(--color-border-error_subtle); --outline-color-brand: var(--color-border-brand); --outline-color-brand_alt: var(--color-border-brand_alt); --outline-color-brand-solid: var(--color-bg-brand-solid); --outline-color-brand-solid_hover: var(--color-bg-brand-solid_hover); } @layer base { /* DARK MODE VARIABLES */ .dark-mode { --color-alpha-white: rgb(12 14 18); --color-alpha-black: rgb(255 255 255); /* UTILITY COLORS */ --color-utility-blue-50: var(--color-blue-950); --color-utility-blue-100: var(--color-blue-900); --color-utility-blue-200: var(--color-blue-800); --color-utility-blue-300: var(--color-blue-700); --color-utility-blue-400: var(--color-blue-600); --color-utility-blue-500: var(--color-blue-500); --color-utility-blue-600: var(--color-blue-400); --color-utility-blue-700: var(--color-blue-300); --color-utility-brand-50: var(--color-brand-950); --color-utility-brand-50_alt: var(--color-utility-neutral-50); --color-utility-brand-100: var(--color-brand-900); --color-utility-brand-100_alt: var(--color-utility-neutral-100); --color-utility-brand-200: var(--color-brand-800); --color-utility-brand-200_alt: var(--color-utility-neutral-200); --color-utility-brand-300: var(--color-brand-700); --color-utility-brand-300_alt: var(--color-utility-neutral-300); --color-utility-brand-400: var(--color-brand-600); --color-utility-brand-400_alt: var(--color-utility-neutral-400); --color-utility-brand-500: var(--color-brand-500); --color-utility-brand-500_alt: var(--color-utility-neutral-500); --color-utility-brand-600: var(--color-brand-400); --color-utility-brand-600_alt: var(--color-utility-neutral-600); --color-utility-brand-700: var(--color-brand-300); --color-utility-brand-700_alt: var(--color-utility-neutral-700); --color-utility-brand-800: var(--color-brand-200); --color-utility-brand-800_alt: var(--color-utility-neutral-800); --color-utility-brand-900: var(--color-brand-100); --color-utility-brand-900_alt: var(--color-utility-neutral-900); --color-utility-neutral-50: var(--color-neutral-900); --color-utility-neutral-100: var(--color-neutral-800); --color-utility-neutral-200: var(--color-neutral-700); --color-utility-neutral-300: var(--color-neutral-700); --color-utility-neutral-400: var(--color-neutral-600); --color-utility-neutral-500: var(--color-neutral-500); --color-utility-neutral-600: var(--color-neutral-400); --color-utility-neutral-700: var(--color-neutral-300); --color-utility-neutral-800: var(--color-neutral-200); --color-utility-neutral-900: var(--color-neutral-100); --color-utility-red-50: var(--color-red-950); --color-utility-red-100: var(--color-red-900); --color-utility-red-200: var(--color-red-800); --color-utility-red-300: var(--color-red-700); --color-utility-red-400: var(--color-red-600); --color-utility-red-500: var(--color-red-500); --color-utility-red-600: var(--color-red-400); --color-utility-red-700: var(--color-red-300); --color-utility-yellow-50: var(--color-yellow-950); --color-utility-yellow-100: var(--color-yellow-900); --color-utility-yellow-200: var(--color-yellow-800); --color-utility-yellow-300: var(--color-yellow-700); --color-utility-yellow-400: var(--color-yellow-600); --color-utility-yellow-500: var(--color-yellow-500); --color-utility-yellow-600: var(--color-yellow-400); --color-utility-yellow-700: var(--color-yellow-300); --color-utility-green-50: var(--color-green-950); --color-utility-green-100: var(--color-green-900); --color-utility-green-200: var(--color-green-800); --color-utility-green-300: var(--color-green-700); --color-utility-green-400: var(--color-green-600); --color-utility-green-500: var(--color-green-500); --color-utility-green-600: var(--color-green-400); --color-utility-green-700: var(--color-green-300); --color-utility-orange-50: var(--color-orange-950); --color-utility-orange-100: var(--color-orange-900); --color-utility-orange-200: var(--color-orange-800); --color-utility-orange-300: var(--color-orange-700); --color-utility-orange-400: var(--color-orange-600); --color-utility-orange-500: var(--color-orange-500); --color-utility-orange-600: var(--color-orange-400); --color-utility-orange-700: var(--color-orange-300); --color-utility-indigo-50: var(--color-indigo-950); --color-utility-indigo-100: var(--color-indigo-900); --color-utility-indigo-200: var(--color-indigo-800); --color-utility-indigo-300: var(--color-indigo-700); --color-utility-indigo-400: var(--color-indigo-600); --color-utility-indigo-500: var(--color-indigo-500); --color-utility-indigo-600: var(--color-indigo-400); --color-utility-indigo-700: var(--color-indigo-300); --color-utility-fuchsia-50: var(--color-fuchsia-950); --color-utility-fuchsia-100: var(--color-fuchsia-900); --color-utility-fuchsia-200: var(--color-fuchsia-800); --color-utility-fuchsia-300: var(--color-fuchsia-700); --color-utility-fuchsia-400: var(--color-fuchsia-600); --color-utility-fuchsia-500: var(--color-fuchsia-500); --color-utility-fuchsia-600: var(--color-fuchsia-400); --color-utility-fuchsia-700: var(--color-fuchsia-300); --color-utility-pink-50: var(--color-pink-950); --color-utility-pink-100: var(--color-pink-900); --color-utility-pink-200: var(--color-pink-800); --color-utility-pink-300: var(--color-pink-700); --color-utility-pink-400: var(--color-pink-600); --color-utility-pink-500: var(--color-pink-500); --color-utility-pink-600: var(--color-pink-400); --color-utility-pink-700: var(--color-pink-300); --color-utility-purple-50: var(--color-purple-950); --color-utility-purple-100: var(--color-purple-900); --color-utility-purple-200: var(--color-purple-800); --color-utility-purple-300: var(--color-purple-700); --color-utility-purple-400: var(--color-purple-600); --color-utility-purple-500: var(--color-purple-500); --color-utility-purple-600: var(--color-purple-400); --color-utility-purple-700: var(--color-purple-300); --color-utility-sky-50: var(--color-sky-950); --color-utility-sky-100: var(--color-sky-900); --color-utility-sky-200: var(--color-sky-800); --color-utility-sky-300: var(--color-sky-700); --color-utility-sky-400: var(--color-sky-600); --color-utility-sky-500: var(--color-sky-500); --color-utility-sky-600: var(--color-sky-400); --color-utility-sky-700: var(--color-sky-300); --color-utility-slate-50: var(--color-slate-950); --color-utility-slate-100: var(--color-slate-900); --color-utility-slate-200: var(--color-slate-800); --color-utility-slate-300: var(--color-slate-700); --color-utility-slate-400: var(--color-slate-600); --color-utility-slate-500: var(--color-slate-500); --color-utility-slate-600: var(--color-slate-400); --color-utility-slate-700: var(--color-slate-300); --color-utility-emerald-50: var(--color-emerald-950); --color-utility-emerald-100: var(--color-emerald-900); --color-utility-emerald-200: var(--color-emerald-800); --color-utility-emerald-300: var(--color-emerald-700); --color-utility-emerald-400: var(--color-emerald-600); --color-utility-emerald-500: var(--color-emerald-500); --color-utility-emerald-600: var(--color-emerald-400); --color-utility-emerald-700: var(--color-emerald-300); --color-utility-amber-50: var(--color-amber-950); --color-utility-amber-100: var(--color-amber-900); --color-utility-amber-200: var(--color-amber-800); --color-utility-amber-300: var(--color-amber-700); --color-utility-amber-400: var(--color-amber-600); --color-utility-amber-500: var(--color-amber-500); --color-utility-amber-600: var(--color-amber-400); --color-utility-amber-700: var(--color-amber-300); --color-text-primary: var(--color-neutral-50); --color-text-tertiary: var(--color-neutral-400); --color-text-error-primary: var(--color-red-400); --color-text-warning-primary: var(--color-yellow-400); --color-text-success-primary: var(--color-green-400); --color-text-white: var(--color-white); --color-text-secondary: var(--color-neutral-300); --color-text-secondary_hover: var(--color-neutral-200); --color-text-tertiary_hover: var(--color-neutral-300); --color-text-brand-secondary: var(--color-neutral-300); --color-text-placeholder: var(--color-neutral-500); --color-text-brand-tertiary: var(--color-neutral-400); --color-text-editor-icon-fg: var(--color-neutral-400); --color-text-editor-icon-fg_active: var(--color-white); --color-text-quaternary: var(--color-neutral-400); --color-text-brand-primary: var(--color-neutral-50); --color-text-primary_on-brand: var(--color-neutral-50); --color-text-secondary_on-brand: var(--color-neutral-300); --color-text-tertiary_on-brand: var(--color-neutral-400); --color-text-quaternary_on-brand: var(--color-neutral-400); --color-text-brand-tertiary_alt: var(--color-neutral-50); --color-text-error-primary_hover: var(--color-red-300); --color-text-brand-secondary_hover: var(--color-neutral-200); --color-border-secondary: var(--color-neutral-800); --color-border-error_subtle: var(--color-red-500); --color-border-primary: var(--color-neutral-700); --color-border-brand: var(--color-brand-400); --color-border-error: var(--color-red-400); --color-border-tertiary: var(--color-neutral-800); --color-border-brand_alt: var(--color-neutral-700); --color-border-secondary_alt: var(--color-neutral-800); --color-fg-secondary: var(--color-neutral-300); --color-fg-warning-primary: var(--color-yellow-500); --color-fg-success-primary: var(--color-green-500); --color-fg-white: var(--color-white); --color-fg-success-secondary: var(--color-green-400); --color-fg-secondary_hover: var(--color-neutral-200); --color-fg-primary: var(--color-white); --color-fg-brand-secondary: var(--color-brand-500); --color-fg-brand-primary: var(--color-brand-500); --color-fg-quaternary: var(--color-neutral-600); --color-fg-quaternary_hover: var(--color-neutral-500); --color-fg-error-primary: var(--color-red-500); --color-fg-warning-secondary: var(--color-yellow-400); --color-fg-error-secondary: var(--color-red-400); --color-fg-tertiary: var(--color-neutral-400); --color-fg-tertiary_hover: var(--color-neutral-300); --color-fg-brand-primary_alt: var(--color-neutral-300); --color-fg-brand-secondary_alt: var(--color-neutral-600); --color-fg-brand-secondary_hover: var(--color-neutral-500); --color-bg-primary: var(--color-neutral-950); --color-bg-tertiary: var(--color-neutral-800); --color-bg-brand-primary: var(--color-brand-500); --color-bg-error-secondary: var(--color-red-600); --color-bg-warning-primary: var(--color-yellow-950); --color-bg-warning-secondary: var(--color-yellow-600); --color-bg-success-primary: var(--color-green-950); --color-bg-success-secondary: var(--color-green-600); --color-bg-brand-solid: var(--color-brand-600); --color-bg-secondary-solid: var(--color-neutral-600); --color-bg-error-solid: var(--color-red-600); --color-bg-warning-solid: var(--color-yellow-600); --color-bg-success-solid: var(--color-green-600); --color-bg-secondary_hover: var(--color-neutral-800); --color-bg-primary_hover: var(--color-neutral-900); --color-bg-active: var(--color-neutral-800); --color-bg-brand-solid_hover: var(--color-brand-500); --color-bg-error-primary: var(--color-red-950); --color-bg-brand-secondary: var(--color-brand-600); --color-bg-secondary: var(--color-neutral-900); --color-bg-quaternary: var(--color-neutral-700); --color-bg-primary_alt: var(--color-bg-secondary); --color-bg-brand-primary_alt: var(--color-bg-secondary); --color-bg-secondary_alt: var(--color-bg-primary); --color-bg-overlay: var(--color-neutral-800); --color-bg-brand-section: var(--color-bg-secondary); --color-bg-brand-section_subtle: var(--color-bg-primary); --color-bg-primary-solid: var(--color-bg-secondary); --color-bg-error-solid_hover: var(--color-red-500); --color-app-store-badge-border: var(--color-white); --color-avatar-styles-bg-neutral: 224 224 224 1; --color-featured-icon-light-fg-brand: var(--color-brand-200); --color-featured-icon-light-fg-error: var(--color-red-200); --color-featured-icon-light-fg-gray: var(--color-neutral-200); --color-featured-icon-light-fg-success: var(--color-green-200); --color-featured-icon-light-fg-warning: var(--color-yellow-200); --color-focus-ring-error: var(--color-red-500); --color-focus-ring: var(--color-brand-500); --color-footer-button-fg: var(--color-neutral-300); --color-footer-button-fg_hover: var(--color-neutral-100); --color-icon-fg-brand: var(--color-neutral-400); --color-icon-fg-brand_on-brand: var(--color-neutral-400); --color-screen-mockup-border: var(--color-neutral-700); --color-slider-handle-bg: var(--color-fg-brand-primary); --color-slider-handle-border: var(--color-bg-primary); --color-toggle-border: var(--color-transparent); --color-toggle-slim-border_pressed-hover: var(--color-transparent); --color-toggle-slim-border_pressed: var(--color-transparent); --color-tooltip-supporting-text: var(--color-neutral-300); /* BACKGROUND PROPERTY COLORS */ --background-color-primary: var(--color-bg-primary); --background-color-tertiary: var(--color-bg-tertiary); --background-color-brand-primary: var(--color-bg-brand-primary); --background-color-error-secondary: var(--color-bg-error-secondary); --background-color-warning-primary: var(--color-bg-warning-primary); --background-color-warning-secondary: var(--color-bg-warning-secondary); --background-color-success-primary: var(--color-bg-success-primary); --background-color-success-secondary: var(--color-bg-success-secondary); --background-color-brand-solid: var(--color-bg-brand-solid); --background-color-secondary-solid: var(--color-bg-secondary-solid); --background-color-error-solid: var(--color-bg-error-solid); --background-color-warning-solid: var(--color-bg-warning-solid); --background-color-success-solid: var(--color-bg-success-solid); --background-color-secondary_hover: var(--color-bg-secondary_hover); --background-color-primary_hover: var(--color-bg-primary_hover); --background-color-active: var(--color-bg-active); --background-color-brand-solid_hover: var(--color-bg-brand-solid_hover); --background-color-error-primary: var(--color-bg-error-primary); --background-color-brand-secondary: var(--color-bg-brand-secondary); --background-color-secondary: var(--color-bg-secondary); --background-color-quaternary: var(--color-bg-quaternary); --background-color-primary_alt: var(--color-bg-primary_alt); --background-color-brand-primary_alt: var(--color-bg-brand-primary_alt); --background-color-secondary_alt: var(--color-bg-secondary_alt); --background-color-overlay: var(--color-bg-overlay); --background-color-brand-section: var(--color-bg-brand-section); --background-color-brand-section_subtle: var(--color-bg-brand-section_subtle); --background-color-primary-solid: var(--color-bg-primary-solid); --background-color-error-solid_hover: var(--color-bg-error-solid_hover); --background-color-border-brand: var(--color-border-brand); --background-color-border-tertiary: var(--color-border-tertiary); --background-color-border-brand_alt: var(--color-border-brand_alt); /* TEXT PROPERTY COLORS */ --text-color-primary: var(--color-text-primary); --text-color-tertiary: var(--color-text-tertiary); --text-color-error-primary: var(--color-text-error-primary); --text-color-warning-primary: var(--color-text-warning-primary); --text-color-success-primary: var(--color-text-success-primary); --text-color-white: var(--color-text-white); --text-color-secondary: var(--color-text-secondary); --text-color-secondary_hover: var(--color-text-secondary_hover); --text-color-tertiary_hover: var(--color-text-tertiary_hover); --text-color-brand-secondary: var(--color-text-brand-secondary); --text-color-placeholder: var(--color-text-placeholder); --text-color-brand-tertiary: var(--color-text-brand-tertiary); --text-color-editor-icon-fg: var(--color-text-editor-icon-fg); --text-color-editor-icon-fg_active: var(--color-text-editor-icon-fg_active); --text-color-quaternary: var(--color-text-quaternary); --text-color-brand-primary: var(--color-text-brand-primary); --text-color-primary_on-brand: var(--color-text-primary_on-brand); --text-color-secondary_on-brand: var(--color-text-secondary_on-brand); --text-color-tertiary_on-brand: var(--color-text-tertiary_on-brand); --text-color-quaternary_on-brand: var(--color-text-quaternary_on-brand); --text-color-brand-tertiary_alt: var(--color-text-brand-tertiary_alt); --text-color-error-primary_hover: var(--color-text-error-primary_hover); --text-color-brand-secondary_hover: var(--color-text-brand-secondary_hover); --text-color-tooltip-supporting-text: var(--color-tooltip-supporting-text); /* BORDER PROPERTY COLORS */ --border-color-primary: var(--color-border-primary); --border-color-secondary: var(--color-border-secondary); --border-color-secondary_alt: var(--color-border-secondary_alt); --border-color-tertiary: var(--color-border-tertiary); --border-color-error: var(--color-border-error); --border-color-error_subtle: var(--color-border-error_subtle); --border-color-brand: var(--color-border-brand); --border-color-brand_alt: var(--color-border-brand_alt); --border-color-brand-solid: var(--color-bg-brand-solid); --border-color-brand-solid_hover: var(--color-bg-brand-solid_hover); /* RING PROPERTY COLORS */ --ring-color-primary: var(--color-border-primary); --ring-color-secondary: var(--color-border-secondary); --ring-color-secondary_alt: var(--color-border-secondary_alt); --ring-color-tertiary: var(--color-border-tertiary); --ring-color-error: var(--color-border-error); --ring-color-error_subtle: var(--color-border-error_subtle); --ring-color-brand: var(--color-border-brand); --ring-color-brand-solid: var(--color-bg-brand-solid); --ring-color-brand-solid_hover: var(--color-bg-brand-solid_hover); --ring-color-brand_alt: var(--color-border-brand_alt); /* OUTLINE PROPERTY COLORS */ --outline-color-primary: var(--color-border-primary); --outline-color-secondary: var(--color-border-secondary); --outline-color-secondary_alt: var(--color-border-secondary_alt); --outline-color-tertiary: var(--color-border-tertiary); --outline-color-error: var(--color-border-error); --outline-color-error_subtle: var(--color-border-error_subtle); --outline-color-brand: var(--color-border-brand); --outline-color-brand-solid: var(--color-bg-brand-solid); --outline-color-brand-solid_hover: var(--color-bg-brand-solid_hover); --outline-color-brand_alt: var(--color-border-brand_alt); } }
Extend Tailwind CSS config
Now, create a globals.css file next to it that imports the theme.css and necessary plugins. It replaces the template's src/styles.css, which you can delete:
@import "tailwindcss"; @import "./theme.css"; @plugin "tailwindcss-animate"; @plugin "tailwindcss-react-aria-components"; @custom-variant dark (&:where(.dark-mode, .dark-mode *)); @custom-variant label (& [data-label]); @custom-variant focus-input-within (&:has(input:focus)); @utility scrollbar-hide { /* For Webkit-based browsers (Chrome, Safari and Opera) */ &::-webkit-scrollbar { display: none; -webkit-appearance: none; } /* For IE, Edge and Firefox */ -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ } @utility transition-inherit-all { transition-property: inherit; transition-duration: inherit; transition-timing-function: inherit; } html, body { font-family: var(--font-body); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-font-variant-ligatures: contextual; font-variant-ligatures: contextual; -webkit-font-kerning: normal; font-kerning: normal; } /* Hide the default expand arrow on Safari. */ details summary::-webkit-details-marker { display: none; } /* Hide default arrows from number inputs. */ /* Chrome, Safari, Edge, Opera */ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } /* Firefox */ input[type="number"] { -moz-appearance: textfield; } /* Hide the default clear button (X) from search inputs. */ input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; } /* Hide the default outline on chart elements. */ .recharts-surface, .recharts-surface g, .recharts-surface path { outline: none; }
Add utility functions
In order to make the development experience slightly better we have added a few utility functions which we'll use throughout our project. Create these files under the src/utils and src/hooks directories.
import { extendTailwindMerge } from "tailwind-merge"; const twMerge = extendTailwindMerge({ extend: { theme: { text: ["display-xs", "display-sm", "display-md", "display-lg", "display-xl", "display-2xl"], }, }, }); /** * This function is a wrapper around the twMerge function. * It is used to merge the classes inside style objects. */ export const cx = twMerge; /** * This function does nothing besides helping us to be able to * sort the classes inside style objects which is not supported * by the Tailwind IntelliSense by default. */ export function sortCx<T extends Record<string, string | number | Record<string, string | number | Record<string, string | number>>>>(classes: T): T { return classes; }
Add a route provider
For client-side routing integration with React Aria components, create a RouteProvider component in src/providers/router-provider.tsx or in your desired location:
import type { PropsWithChildren } from "react"; import { useRouter } from "@tanstack/react-router"; import { RouterProvider } from "react-aria-components"; export const RouteProvider = ({ children }: PropsWithChildren) => { const router = useRouter(); return ( <RouterProvider navigate={(href) => router.navigate({ to: href })} useHref={(href) => router.buildLocation({ to: href }).href}> {children} </RouterProvider> ); };
navigate hands React Aria links, such as a Button with an href, to TanStack Router, so they navigate client-side instead of reloading the page, and useHref asks the router to build the href each link renders.
Add a theme provider
For dark mode support, add a ThemeProvider component in src/providers/theme-provider.tsx or in your desired location:
import type { ReactNode } from "react"; import { createContext, useContext, useEffect, useState } from "react"; type Theme = "light" | "dark" | "system"; interface ThemeContextType { theme: Theme; setTheme: (theme: Theme) => void; } const ThemeContext = createContext<ThemeContextType | undefined>(undefined); export const useTheme = (): ThemeContextType => { const context = useContext(ThemeContext); if (context === undefined) { throw new Error("useTheme must be used within a ThemeProvider"); } return context; }; interface ThemeProviderProps { children: ReactNode; /** * The class to add to the root element when the theme is dark * @default "dark-mode" */ darkModeClass?: string; /** * The default theme to use if no theme is stored in localStorage * @default "system" */ defaultTheme?: Theme; /** * The key to use to store the theme in localStorage * @default "ui-theme" */ storageKey?: string; } export const ThemeProvider = ({ children, defaultTheme = "system", storageKey = "ui-theme", darkModeClass = "dark-mode" }: ThemeProviderProps) => { const [theme, setTheme] = useState<Theme>(() => { if (typeof window !== "undefined") { const savedTheme = localStorage.getItem(storageKey) as Theme | null; return savedTheme || defaultTheme; } return defaultTheme; }); useEffect(() => { const applyTheme = () => { const root = window.document.documentElement; if (theme === "system") { const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; root.classList.toggle(darkModeClass, systemTheme === "dark"); localStorage.removeItem(storageKey); } else { root.classList.toggle(darkModeClass, theme === "dark"); localStorage.setItem(storageKey, theme); } }; applyTheme(); // Listen for system theme changes const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); const handleChange = () => { if (theme === "system") { applyTheme(); } }; mediaQuery.addEventListener("change", handleChange); return () => mediaQuery.removeEventListener("change", handleChange); }, [theme]); return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>; };
Update the root route
Update the root route in src/routes/__root.tsx to include the providers, the Inter font and the global styles:
import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router"; import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; import { TanStackDevtools } from "@tanstack/react-devtools"; import { RouteProvider } from "@/providers/router-provider"; import { ThemeProvider } from "@/providers/theme-provider"; import appCss from "@/styles/globals.css?url"; // Applies the saved theme before the page paints, so a dark-mode visitor never sees a light flash. const themeScript = ` const theme = localStorage.getItem("ui-theme"); if (theme === "dark" || (!theme && window.matchMedia("(prefers-color-scheme: dark)").matches)) { document.documentElement.classList.add("dark-mode"); } `; export const Route = createRootRoute({ head: () => ({ meta: [ { charSet: "utf-8", }, { name: "viewport", content: "width=device-width, initial-scale=1", }, { title: "TanStack Start Starter", }, ], links: [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }, { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,400..700;1,14..32,400..700&display=swap" }, { rel: "stylesheet", href: appCss }, ], scripts: [{ children: themeScript }], }), shellComponent: RootDocument, }); function RootDocument({ children }: { children: React.ReactNode }) { return ( <html lang="en" suppressHydrationWarning> <head> <HeadContent /> </head> <body className="bg-primary antialiased"> <ThemeProvider> <RouteProvider>{children}</RouteProvider> </ThemeProvider> <TanStackDevtools config={{ position: "bottom-right", }} plugins={[ { name: "Tanstack Router", render: <TanStackRouterDevtoolsPanel />, }, ]} /> <Scripts /> </body> </html> ); }
The inline script runs before React hydrates, so it adds the dark-mode class before the first paint; suppressHydrationWarning tells React that the class on <html> is expected to differ from the server-rendered markup.
Ready to go!
Great! You're all set to start using Untitled UI components.
If something is missing, you can copy/paste what you need into your project directly from individual components pages.
Need help? Check our GitHub repository for examples, or open an issue if you run into any problems. Our community is here to help!
Using Untitled UI components
After setting up the providers, you can use Untitled UI components in any route:
import { createFileRoute } from "@tanstack/react-router"; import { Button } from "@/components/base/buttons/button"; import { Input } from "@/components/base/input/input"; export const Route = createFileRoute("/")({ component: Home, }); function Home() { const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); const name = formData.get("name") as string; console.log(name); }; return ( <main className="container mx-auto p-4"> <h1 className="text-2xl font-bold">Welcome to Untitled UI + TanStack Start</h1> <form onSubmit={handleSubmit} className="mt-4"> <Input label="Name" name="name" placeholder="Enter your name" /> <Button type="submit" className="mt-2">Submit</Button> </form> </main> ); }
Dark mode
Untitled UI supports dark mode out of the box. The implementation uses CSS variables to provide a seamless experience.
Theme toggle
Create a toggle to switch between light and dark mode:
import { Moon01, Sun } from "@untitledui/icons"; import { Button } from "@/components/base/buttons/button"; import { useTheme } from "@/providers/theme-provider"; export function ThemeToggle() { const { setTheme } = useTheme(); return ( <Button aria-label="Toggle theme" color="tertiary" size="sm" iconLeading={ <> <Moon01 data-icon className="dark:hidden" /> <Sun data-icon className="hidden dark:block" /> </> } onClick={() => setTheme(document.documentElement.classList.contains("dark-mode") ? "light" : "dark")} /> ); }
TanStack Start renders pages on the server, where the saved theme isn't known. So the toggle picks its icon with the dark: variant rather than from state, which keeps the server and browser markup identical, and flips whichever theme is showing, including one that follows the system setting.
FAQs
Please refer to our frequently asked questions page for more.
You can either use our CLI command (npx untitledui@latest init --tanstack) to create a new project from our TanStack Start starter kit, or add Untitled UI to an existing project manually. The manual setup installs a few packages, switches the TypeScript lib to ESNext, adds our theme and global styles, and wires the route and theme providers into src/routes/__root.tsx.
Yes. Run npx untitledui@latest add with the components you need, for example npx untitledui@latest add button input. The CLI detects the project and writes the components under src/components, using the @/ alias from your tsconfig.json.
The TanStack Start template compiles against the ES2022 library, and some components use newer JavaScript methods, such as Array.prototype.toReversed() in the chart components. With ES2022, TypeScript reports those as errors; with ESNext, they type-check.
Yes. Untitled UI components render on the server and hydrate in the browser like any other React component. For dark mode, the inline script in the root route applies the saved theme before the first paint, and the theme toggle picks its icon with CSS so the server and browser render the same markup.
Run npm run build. Untitled UI adds nothing to the build: the theme and global styles are compiled by the Tailwind CSS Vite plugin, and the root route links the resulting stylesheet through its ?url import.