Theming
Uai components read the standard shadcn theme tokens, so they follow whatever theme your project defines. The Uai theme fills those tokens with a warm graphite palette.
Install the theme
Every component installs the theme as a dependency. To set it up on its own, run the command once. The CLI writes the light and dark tokens into your global CSS and adds tw-animate-css.
Already have a shadcn theme you like? Skip this step. Components use your background, card, primary, and muted-foreground tokens. Add the four Uai extensions from the CSS below: subtle-foreground, border-strong, success, and warning.
Manual installation
Install the dependencies.
bun add class-variance-authority clsx tailwind-merge tw-animate-css lucide-reactAdd the theme to your global CSS file, such as
app/globals.css.@import "tailwindcss";@import "tw-animate-css";@custom-variant dark (&:is(.dark *));@theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); --color-card-foreground: var(--card-foreground); --color-popover: var(--popover); --color-popover-foreground: var(--popover-foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); --color-subtle-foreground: var(--subtle-foreground); --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); --color-success: var(--success); --color-warning: var(--warning); --color-success-ink: var(--success-ink); --color-warning-ink: var(--warning-ink); --color-destructive-ink: var(--destructive-ink); --color-primary-ink: var(--primary-ink); --color-border: var(--border); --color-border-strong: var(--border-strong); --color-input: var(--input); --color-ring: var(--ring); --ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1); --animate-shimmer: shimmer 2s linear infinite; --animate-skeleton-shimmer: skeleton-shimmer 1.6s ease-in-out infinite; --animate-indeterminate: indeterminate 1.4s cubic-bezier(0.65, 0, 0.35, 1) infinite; --animate-ring-pulse: ring-pulse 1.8s ease-out infinite; --animate-grow-x: grow-x 480ms cubic-bezier(0.23, 1, 0.32, 1) backwards; --animate-grow-y: grow-y 480ms cubic-bezier(0.23, 1, 0.32, 1) backwards; --animate-shake: shake 260ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both;}:root { --radius: 0.875rem; --background: oklch(0.975 0.001 260); --foreground: oklch(0.247 0.006 258); --card: oklch(1 0 0); --card-foreground: oklch(0.247 0.006 258); --popover: oklch(1 0 0); --popover-foreground: oklch(0.247 0.006 258); --primary: oklch(0.57 0.185 257); --primary-foreground: oklch(1 0 0); --secondary: oklch(0.94 0.003 260); --secondary-foreground: oklch(0.247 0.006 258); --muted: oklch(0.94 0.003 260); --muted-foreground: oklch(0.43 0.01 260); --subtle-foreground: oklch(0.52 0.01 260); --accent: oklch(0.94 0.003 260); --accent-foreground: oklch(0.247 0.006 258); --destructive: oklch(0.58 0.2 23); --success: oklch(0.6 0.15 153); --warning: oklch(0.66 0.16 55); --success-ink: oklch(0.5 0.15 153); --warning-ink: oklch(0.54 0.16 55); --destructive-ink: oklch(0.54 0.2 23); --primary-ink: oklch(0.485 0.185 257); --border: oklch(0.915 0.003 260); --border-strong: oklch(0.84 0.005 260); --input: oklch(0.915 0.003 260); --ring: oklch(0.57 0.185 257);}.dark { --background: oklch(0.226 0.004 264); --foreground: oklch(0.964 0.002 248); --card: oklch(0.26 0.006 271); --card-foreground: oklch(0.964 0.002 248); --popover: oklch(0.26 0.006 271); --popover-foreground: oklch(0.964 0.002 248); --primary: oklch(0.57 0.185 257); --primary-foreground: oklch(1 0 0); --secondary: oklch(0.293 0.006 271); --secondary-foreground: oklch(0.964 0.002 248); --muted: oklch(0.293 0.006 271); --muted-foreground: oklch(0.79 0.008 261); --subtle-foreground: oklch(0.69 0.01 264); --accent: oklch(0.293 0.006 271); --accent-foreground: oklch(0.964 0.002 248); --destructive: oklch(0.666 0.18 21); --success: oklch(0.705 0.154 154); --warning: oklch(0.746 0.156 56); --success-ink: oklch(0.705 0.154 154); --warning-ink: oklch(0.746 0.156 56); --destructive-ink: oklch(0.71 0.18 21); --primary-ink: oklch(0.72 0.185 257); --border: oklch(0.315 0.006 268); --border-strong: oklch(0.39 0.007 268); --input: oklch(0.315 0.006 268); --ring: oklch(0.57 0.185 257);}@layer base { * { @apply border-border; }}@keyframes shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; }}@keyframes skeleton-shimmer { from { background-position: 150% 0; } to { background-position: -50% 0; }}@keyframes indeterminate { from { transform: translateX(-100%); } to { transform: translateX(290%); }}@keyframes ring-pulse { 0% { box-shadow: 0 0 0 0 color-mix(in oklab, currentColor 45%, transparent); } 70%, 100% { box-shadow: 0 0 0 5px transparent; }}@keyframes grow-x { from { transform: scaleX(0); }}@keyframes grow-y { from { transform: scaleY(0); }}@keyframes shake { 20%, 60% { translate: -4px 0; } 40%, 80% { translate: 4px 0; }}@utility shimmer-text { background-image: linear-gradient(90deg, var(--subtle-foreground) 0%, var(--subtle-foreground) 35%, var(--foreground) 50%, var(--subtle-foreground) 65%, var(--subtle-foreground) 100%); background-size: 200% 100%; background-clip: text; color: transparent; animation: var(--animate-shimmer); @media (prefers-reduced-motion: reduce) { animation: none; background: none; color: var(--foreground); }}Create
lib/uai-utils.ts. Every component merges yourclassNamethrough this helper.import { type ClassValue, clsx } from "clsx";import { twMerge } from "tailwind-merge";export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs));}Copy a component's source from its page into
components/ui/uai.
Tokens
| Token | Utility | Used for |
|---|---|---|
--background | bg-background | Page canvas behind cards. |
--card | bg-card | Cards, panels, and fields. |
--popover | bg-popover | Menus, dialogs, and popovers. |
--muted | bg-muted | Tonal fills, chips, and wells. |
--accent | hover:bg-accent | Hover and selected rows. |
--secondary | bg-secondary | Secondary pill buttons. |
--foreground | text-foreground | Headings and primary labels. |
--muted-foreground | text-muted-foreground | Body copy and labels. |
--subtle-foreground | text-subtle-foreground | Tertiary metadata: counts, timestamps, captions. |
--primary | bg-primary | The one primary action per surface. |
--border | border | Hairlines. |
--border-strong | border-border-strong | Focused fields and menu outlines. |
--ring | outline-ring | Keyboard focus. |
--success | bg-success/14 text-success | Positive status. |
--warning | bg-warning/14 text-warning | Cautionary status. |
--destructive | bg-destructive/14 | Errors and destructive actions. |
Customize
Change a token to restyle every component at once. Override both themes if you support dark mode.
:root { --primary: oklch(0.6 0.2 300); --radius: 0.5rem;}.dark { --primary: oklch(0.7 0.18 300);}Change a single instance with className. Your classes are merged last, so they win over the component's own.
<MetricCard variant="compact" className="rounded-none border-0 bg-muted"> <MetricCardLabel className="text-foreground">Revenue</MetricCardLabel> <MetricCardValue>$48,290</MetricCardValue></MetricCard>