Customization
Tac UI is fully customizable through CSS custom properties. Override any design token to match your brand identity without modifying the component source code.
How It Works
All Tac UI components consume design tokens as CSS custom properties (--variable-name). These are injected at the :root level by the TacProvider and the Tailwind preset. You can override any token by re-declaring the variable in your own CSS.
Because CSS custom properties follow the cascade, your overrides always win. This means you can customize globally, per-theme, or even per-component.
Global Overrides
The simplest way to customize is to override tokens in your globals.css after the Tailwind import. Changes apply to both light and dark themes.
/* globals.css */
@import "tailwindcss";
@config "./tailwind.config.ts";
/* Global token overrides */
:root {
--primary: #96784A;
--primary-hover: #84693C;
--primary-foreground: #FFFFFF;
--radius-m: 0.75rem;
--radius-lg: 1rem;
}Per-Theme Overrides
To customize tokens differently for light and dark modes, use the data-theme attribute selectors.
/* Light mode overrides */
[data-theme="light"] {
--primary: #2563EB;
--background: #FAFAFA;
--border: #D1D5DB;
}
/* Dark mode overrides */
[data-theme="dark"] {
--primary: #60A5FA;
--background: #0A0A0A;
--border: #27272A;
}Per-Component Overrides
You can scope token overrides to specific components using wrapper classes or inline styles. Component-level tokens (like --btn-md-height) let you fine-tune individual components.
{/* Override tokens for a specific section */}
<div style={{ '--primary': '#059669', '--primary-hover': '#047857' } as React.CSSProperties}>
<Button>Green Button</Button>
</div>
{/* Or use a CSS class */}
<div className="brand-section">
<Button>Branded Button</Button>
</div>.brand-section {
--primary: #059669;
--primary-hover: #047857;
--radius-m: 9999px; /* pill-shaped buttons */
}Token Reference
Colors
Core color tokens that define the overall look of your application.
| Prop | Type | Default | Description |
|---|---|---|---|
| --primary | color | #4E657E | Primary brand color used for buttons, links, and accents. |
| --primary-hover | color | #3E5165 | Hover state of the primary color. |
| --primary-foreground | color | #FFFFFF | Text color on primary backgrounds. |
| --background | color | #FFFFFF | Page background color. |
| --foreground | color | #0F172A | Default text color. |
| --secondary | color | #F1F5F9 | Secondary surface color for subtle backgrounds. |
| --border | color | #E2E8F0 | Default border color. |
| --muted-foreground | color | #64748B | Subdued text (descriptions, placeholders). |
| --point | color | #96784A | Accent color for highlights and callouts (use sparingly). |
Status Colors
Semantic colors for feedback states. Each has a matching -bg and -foreground variant.
| Prop | Type | Default | Description |
|---|---|---|---|
| --success | color | #10B981 | Success state color. |
| --warning | color | #F59E0B | Warning state color. |
| --error | color | #EF4444 | Error/destructive state color. |
| --info | color | #3B82F6 | Informational state color. |
Spacing
Consistent spacing scale used for padding, margins, and gaps.
| Prop | Type | Default | Description |
|---|---|---|---|
| --spacing-xs | length | 0.25rem | 4px - Tight spacing (icon gaps). |
| --spacing-sm | length | 0.5rem | 8px - Small spacing. |
| --spacing-m | length | 0.75rem | 12px - Medium spacing. |
| --spacing-lg | length | 1rem | 16px - Large spacing. |
| --spacing-xl | length | 1.5rem | 24px - Extra large spacing. |
| --spacing-2xl | length | 2rem | 32px - Section-level spacing. |
Border Radius
Rounding scale from sharp to fully rounded.
| Prop | Type | Default | Description |
|---|---|---|---|
| --radius-none | length | 0 | No rounding. |
| --radius-sm | length | 0.25rem | Subtle rounding (badges, chips). |
| --radius-m | length | 0.5rem | Default rounding (inputs, buttons). |
| --radius-lg | length | 0.75rem | Larger rounding (cards, modals). |
| --radius-xl | length | 1rem | Extra large rounding. |
| --radius-pill | length | 9999px | Fully rounded (pills, avatars). |
Motion
Duration and easing tokens for animations and transitions.
| Prop | Type | Default | Description |
|---|---|---|---|
| --duration-instant | time | 50ms | Immediate feedback. |
| --duration-fast | time | 150ms | Quick transitions (hover, focus). |
| --duration-normal | time | 250ms | Standard animations. |
| --duration-slow | time | 400ms | Deliberate animations (modals, drawers). |
| --ease-standard | easing | cubic-bezier(0.4, 0, 0.2, 1) | Default easing curve. |
| --ease-out | easing | cubic-bezier(0, 0, 0.2, 1) | Deceleration curve (enter animations). |
Component Tokens
In addition to global tokens, each component has its own set of tokens for granular control. These follow the pattern --component-property.
/* Button size overrides */
:root {
--btn-md-height: 2.75rem; /* taller buttons */
--btn-md-px: 1.5rem; /* wider padding */
--btn-md-font-size: 0.9375rem;
--btn-md-radius: 0.75rem;
}
/* Input overrides */
:root {
--input-height: 3rem;
--input-radius: 0.75rem;
}
/* Card overrides */
:root {
--card-padding: 1.5rem;
--card-radius: 1rem;
}
/* Dialog overrides */
:root {
--dialog-radius: 1.25rem;
}Programmatic Access
You can also access and generate tokens programmatically using the @tac-ui/tokens package.
import { semanticTokens, spacing, radius, motion } from '@tac-ui/tokens';
import { generateCSSVariables } from '@tac-ui/tokens/web/css-variables';
// Get all CSS variables for a given mode
const lightVars = generateCSSVariables('light');
const darkVars = generateCSSVariables('dark');
// Access raw token values
console.log(semanticTokens.light.primary); // '#4E657E'
console.log(spacing.lg); // 16
console.log(radius.m); // 8All Variables — Light Mode
Copy this block into your globals.css to override any variable. All values below are the built-in light mode defaults.
/* globals.css — Light mode (all variables) */
:root, [data-theme="light"] {
/* ── Typography ── */
--font-primary: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
--font-secondary: 'Pretendard', sans-serif;
/* ── Core Colors ── */
--background: #FFFFFF;
--background-subtle: #F8FAFC;
--foreground: #0F172A;
--primary: #4E657E;
--primary-hover: #3E5165;
--primary-foreground: #FFFFFF;
--secondary: #F1F5F9;
--secondary-foreground: #0F172A;
--tertiary: #E2E8F0;
--muted: #F1F5F9;
--muted-foreground: #64748B;
--card: #FFFFFF;
--card-foreground: #0F172A;
--border: #E2E8F0;
--input: #E2E8F0;
--point: #96784A;
--point-hover: #C0B56A;
--point-foreground: #FFFFFF;
/* ── Status Colors ── */
--success: #10B981;
--success-bg: #D1FAE5;
--success-foreground: #065F46;
--warning: #F59E0B;
--warning-bg: #FEF3C7;
--warning-foreground: #92400E;
--error: #EF4444;
--error-bg: #FEE2E2;
--error-foreground: #991B1B;
--info: #3B82F6;
--info-bg: #DBEAFE;
--info-foreground: #1E40AF;
--shadow-color: rgba(0, 0, 0, 0.06);
/* ── Gray Scale ── */
--gray-50: #F8FAFC;
--gray-100: #F1F5F9;
--gray-200: #E2E8F0;
--gray-300: #CBD5E1;
--gray-400: #94A3B8;
--gray-500: #64748B;
--gray-600: #475569;
--gray-700: #334155;
--gray-800: #1E293B;
--gray-900: #0F172A;
/* ── Spacing ── */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-m: 0.75rem;
--spacing-lg: 1rem;
--spacing-xl: 1.5rem;
--spacing-2xl: 2rem;
/* ── Border Radius ── */
--radius-none: 0;
--radius-sm: 0.25rem;
--radius-m: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--radius-pill: 9999px;
/* ── Elevation ── */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-m: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
/* ── Motion ── */
--duration-instant: 50ms;
--duration-fast: 150ms;
--duration-normal: 250ms;
--duration-slow: 400ms;
--duration-complex: 600ms;
--ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
/* ── Chart ── */
--chart-1: #4E657E;
--chart-2: #96784A;
--chart-3: #10B981;
--chart-4: #F59E0B;
--chart-5: #EF4444;
--chart-6: #8B5CF6;
--chart-7: #EC4899;
--chart-8: #14B8A6;
/* ── Z-Index ── */
--z-dropdown: 1000;
--z-sticky: 1100;
--z-overlay: 1200;
--z-modal: 1300;
--z-popover: 1400;
--z-tooltip: 1500;
--z-toast: 1600;
}All Variables — Dark Mode
Dark mode overrides. Only color-related tokens change between themes — spacing, radius, motion, and component tokens remain the same.
/* globals.css — Dark mode (color overrides) */
[data-theme="dark"] {
/* ── Core Colors ── */
--background: #09090B;
--background-subtle: #0F0F12;
--foreground: #FAFAFA;
--primary: #8AA3B8;
--primary-hover: #A0B5C8;
--primary-foreground: #0E1419;
--secondary: #27272A;
--secondary-foreground: #FAFAFA;
--tertiary: #1C1C1F;
--muted: #18181B;
--muted-foreground: #A1A1AA;
--card: #111113;
--card-foreground: #FAFAFA;
--border: #27272A;
--input: #1C1C1F;
--point: #E5DCA0;
--point-hover: #D4BA80;
--point-foreground: #222222;
/* ── Status Colors ── */
--success: #34D399;
--success-bg: rgba(52, 211, 153, 0.1);
--success-foreground: #34D399;
--warning: #FBBF24;
--warning-bg: rgba(251, 191, 36, 0.1);
--warning-foreground: #FBBF24;
--error: #F87171;
--error-bg: rgba(248, 113, 113, 0.1);
--error-foreground: #F87171;
--info: #60A5FA;
--info-bg: rgba(96, 165, 250, 0.1);
--info-foreground: #60A5FA;
--shadow-color: rgba(0, 0, 0, 0.5);
/* ── Gray Scale ── */
--gray-50: #18181B;
--gray-100: #1F1F23;
--gray-200: #27272A;
--gray-300: #3F3F46;
--gray-400: #52525B;
--gray-500: #71717A;
--gray-600: #A1A1AA;
--gray-700: #D4D4D8;
--gray-800: #E4E4E7;
--gray-900: #F4F4F5;
}All Variables — Component Tokens
Component-level tokens for fine-grained control. These are theme-independent and apply to both light and dark modes.
/* globals.css — Component tokens (all variables) */
:root {
/* ── Button ── */
--btn-sm-height: 2rem;
--btn-sm-px: 0.75rem;
--btn-sm-font-size: 0.8125rem;
--btn-sm-radius: 6px;
--btn-md-height: 2.5rem;
--btn-md-px: 1rem;
--btn-md-font-size: 0.875rem;
--btn-md-radius: 8px;
--btn-lg-height: 2.75rem;
--btn-lg-px: 1.5rem;
--btn-lg-font-size: 1rem;
--btn-lg-radius: 8px;
--btn-icon-size: 16px;
/* ── Input ── */
--input-height: 2.5rem;
--input-px: 0.75rem;
--input-py: 0.5rem;
--input-font-size: 0.875rem;
--input-radius: 8px;
--input-icon-size: 16px;
--input-icon-padding: 2.5rem;
/* ── Card ── */
--card-padding: 1.5rem;
--card-gap: 0.75rem;
--card-radius: 12px;
--card-title-size: 1rem;
--card-body-size: 0.875rem;
/* ── Badge ── */
--badge-px: 0.625rem;
--badge-py: 0.125rem;
--badge-font-size: 0.75rem;
--badge-radius: 9999px;
/* ── Checkbox ── */
--checkbox-size: 18px;
--checkbox-radius: 4px;
--checkbox-border-width: 2px;
--checkbox-icon-size: 12px;
--checkbox-gap: 0.5rem;
--checkbox-label-size: 0.875rem;
/* ── Radio ── */
--radio-size: 18px;
--radio-border-width: 2px;
--radio-checked-border-width: 5px;
--radio-gap: 0.5rem;
--radio-label-size: 0.875rem;
/* ── Switch ── */
--switch-width: 44px;
--switch-height: 24px;
--switch-thumb-size: 20px;
--switch-thumb-offset: 2px;
/* ── Chip ── */
--chip-px: 0.75rem;
--chip-py: 0.25rem;
--chip-font-size: 0.8125rem;
--chip-radius: 9999px;
--chip-icon-size: 14px;
/* ── Tabs ── */
--tab-primary-px: 1rem;
--tab-primary-py: 0.75rem;
--tab-primary-font-size: 0.875rem;
--tab-primary-indicator: 2px;
/* ── Slider ── */
--slider-track-height: 6px;
--slider-thumb-size: 20px;
--slider-thumb-border: 3px;
/* ── Tooltip ── */
--tooltip-delay: 150ms;
--tooltip-padding: 1rem;
--tooltip-radius: 0.75rem;
--tooltip-simple-px: 0.5rem;
--tooltip-simple-py: 0.375rem;
--tooltip-simple-font-size: 0.75rem;
--tooltip-simple-radius: 6px;
/* ── Avatar ── */
--avatar-sm: 32px;
--avatar-md: 40px;
--avatar-lg: 48px;
--avatar-xl: 64px;
/* ── Dialog ── */
--dialog-width: 400px;
--dialog-radius: 12px;
--dialog-title-size: 1.125rem;
--dialog-desc-size: 0.875rem;
/* ── Snackbar ── */
--snackbar-radius: 12px;
--snackbar-gap: 0.75rem;
--snackbar-icon-size: 20px;
--snackbar-message-size: 0.875rem;
/* ── Progress ── */
--progress-height: 8px;
--progress-radius: 9999px;
/* ── Divider ── */
--divider-thickness: 1px;
--divider-thick: 2px;
/* ── Toggle ── */
--toggle-size: 40px;
--toggle-radius: 12px;
--toggle-icon-size: 18px;
}