Theme
Configure light and dark themes with semantic tokens. You can use Kalki defaults or define your own token contract in a project-level global.css.
Quick Start
Use Kalki's default token set by importing one stylesheet.
// app/layout.tsx
import 'kalki-design/styles.css'
// app/layout.tsx
import 'kalki-design/styles.css'
Dark Mode Setup
Add next-themes so the .dark class can switch your token values.
import { ThemeProvider } from 'next-themes'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="light" enableSystem>
{children}
</ThemeProvider>
</body>
</html>
)
}
import { ThemeProvider } from 'next-themes'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="light" enableSystem>
{children}
</ThemeProvider>
</body>
</html>
)
}
Custom global.css Theme
For full control, define tokens in your own global.css. Keep semantic names unchanged so all Kalki components continue working.
@import "tailwindcss";
@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-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-chart-6: var(--chart-6);
--color-chart-positive: var(--chart-positive);
--color-chart-negative: var(--chart-negative);
--color-chart-neutral: var(--chart-neutral);
--color-chart-warning: var(--chart-warning);
--color-chart-info: var(--chart-info);
--radius-sm: calc(var(--radius) * 0.5);
--radius-md: calc(var(--radius) * 0.75);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.25);
}
:root {
--radius: 0.5rem;
--background: #ffffff;
--foreground: #09090b;
--card: #ffffff;
--card-foreground: #09090b;
--popover: #ffffff;
--popover-foreground: #09090b;
--primary: #232122;
--primary-foreground: #ffffff;
--secondary: #f4f4f5;
--secondary-foreground: #27272a;
--muted: #f4f4f5;
--muted-foreground: #4b5563;
--accent: #f4f4f5;
--accent-foreground: #27272a;
--destructive: #dc2626;
--destructive-foreground: #ffffff;
--success: #16a34a;
--success-foreground: #09090b;
--warning: #f59e0b;
--warning-foreground: #09090b;
--info: #0ea5e9;
--info-foreground: #09090b;
--border: #e4e4e7;
--input: #e4e4e7;
--ring: #c4c4c8;
--chart-1: #232122;
--chart-2: #3b82f6;
--chart-3: #0ea5e9;
--chart-4: #16a34a;
--chart-5: #f59e0b;
--chart-6: #a855f7;
--chart-positive: var(--success);
--chart-negative: var(--destructive);
--chart-neutral: var(--muted-foreground);
--chart-warning: var(--warning);
--chart-info: var(--info);
}
.dark {
--background: #09090b;
--foreground: #fafafa;
--card: #18181b;
--card-foreground: #fafafa;
--popover: #18181b;
--popover-foreground: #fafafa;
--primary: #fafafa;
--primary-foreground: #232122;
--secondary: #27272a;
--secondary-foreground: #fafafa;
--muted: #27272a;
--muted-foreground: #a1a1aa;
--accent: #27272a;
--accent-foreground: #fafafa;
--destructive: #f87171;
--destructive-foreground: #09090b;
--success: #4ade80;
--success-foreground: #09090b;
--warning: #fbbf24;
--warning-foreground: #09090b;
--info: #38bdf8;
--info-foreground: #09090b;
--border: #27272a;
--input: #27272a;
--ring: #71717a;
--chart-1: #fafafa;
--chart-2: #60a5fa;
--chart-3: #38bdf8;
--chart-4: #4ade80;
--chart-5: #fbbf24;
--chart-6: #c084fc;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
@import "tailwindcss";
@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-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-chart-6: var(--chart-6);
--color-chart-positive: var(--chart-positive);
--color-chart-negative: var(--chart-negative);
--color-chart-neutral: var(--chart-neutral);
--color-chart-warning: var(--chart-warning);
--color-chart-info: var(--chart-info);
--radius-sm: calc(var(--radius) * 0.5);
--radius-md: calc(var(--radius) * 0.75);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.25);
}
:root {
--radius: 0.5rem;
--background: #ffffff;
--foreground: #09090b;
--card: #ffffff;
--card-foreground: #09090b;
--popover: #ffffff;
--popover-foreground: #09090b;
--primary: #232122;
--primary-foreground: #ffffff;
--secondary: #f4f4f5;
--secondary-foreground: #27272a;
--muted: #f4f4f5;
--muted-foreground: #4b5563;
--accent: #f4f4f5;
--accent-foreground: #27272a;
--destructive: #dc2626;
--destructive-foreground: #ffffff;
--success: #16a34a;
--success-foreground: #09090b;
--warning: #f59e0b;
--warning-foreground: #09090b;
--info: #0ea5e9;
--info-foreground: #09090b;
--border: #e4e4e7;
--input: #e4e4e7;
--ring: #c4c4c8;
--chart-1: #232122;
--chart-2: #3b82f6;
--chart-3: #0ea5e9;
--chart-4: #16a34a;
--chart-5: #f59e0b;
--chart-6: #a855f7;
--chart-positive: var(--success);
--chart-negative: var(--destructive);
--chart-neutral: var(--muted-foreground);
--chart-warning: var(--warning);
--chart-info: var(--info);
}
.dark {
--background: #09090b;
--foreground: #fafafa;
--card: #18181b;
--card-foreground: #fafafa;
--popover: #18181b;
--popover-foreground: #fafafa;
--primary: #fafafa;
--primary-foreground: #232122;
--secondary: #27272a;
--secondary-foreground: #fafafa;
--muted: #27272a;
--muted-foreground: #a1a1aa;
--accent: #27272a;
--accent-foreground: #fafafa;
--destructive: #f87171;
--destructive-foreground: #09090b;
--success: #4ade80;
--success-foreground: #09090b;
--warning: #fbbf24;
--warning-foreground: #09090b;
--info: #38bdf8;
--info-foreground: #09090b;
--border: #27272a;
--input: #27272a;
--ring: #71717a;
--chart-1: #fafafa;
--chart-2: #60a5fa;
--chart-3: #38bdf8;
--chart-4: #4ade80;
--chart-5: #fbbf24;
--chart-6: #c084fc;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
Token Convention
Keep semantic names stable and map your brand/colors into these slots.
background / foreground
App canvas and default text.
card / card-foreground
Elevated surfaces and text inside cards.
popover / popover-foreground
Menus, dialogs, and overlays.
primary / primary-foreground
Primary actions and high-attention CTAs.
secondary / secondary-foreground
Secondary actions and subtle controls.
muted / muted-foreground
Low-emphasis surfaces and supporting text.
destructive / destructive-foreground
Errors, dangerous actions, delete flows.
success / warning / info
Status semantics, never brand replacements.
border / input / ring
Structure, form fields, and focus visibility.
Theme Guardrails
Themeable
- Surface, text, border, focus, and semantic feedback tokens.
- Brand accent for primary CTA, selected, and focus moments.
- Chart palette and chart semantic aliases.
Avoid
- Hardcoded hex inside component files.
- Using destructive/success colors as decorative brand colors.
- Changing semantic token names across apps.
Add New Token
Add the token in :root and .dark, then map it inside @theme inline.
/* 1) Define token values in both modes */
:root {
--brand-accent: #6440b6;
--brand-accent-foreground: #ffffff;
}
.dark {
--brand-accent: #8c66e3;
--brand-accent-foreground: #09090b;
}
/* 2) Expose token to Tailwind v4 */
@theme inline {
--color-brand-accent: var(--brand-accent);
--color-brand-accent-foreground: var(--brand-accent-foreground);
}
/* 3) Use in UI */
.marketing-chip {
background: var(--brand-accent);
color: var(--brand-accent-foreground);
}
/* 1) Define token values in both modes */
:root {
--brand-accent: #6440b6;
--brand-accent-foreground: #ffffff;
}
.dark {
--brand-accent: #8c66e3;
--brand-accent-foreground: #09090b;
}
/* 2) Expose token to Tailwind v4 */
@theme inline {
--color-brand-accent: var(--brand-accent);
--color-brand-accent-foreground: var(--brand-accent-foreground);
}
/* 3) Use in UI */
.marketing-chip {
background: var(--brand-accent);
color: var(--brand-accent-foreground);
}
Component State Tokens
Keep interaction states semantic and consistent across components.
<button
className="
rounded-md
bg-primary text-primary-foreground
hover:bg-accent hover:text-accent-foreground
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
disabled:bg-muted disabled:text-muted-foreground disabled:cursor-not-allowed
"
>
Save changes
</button>
<button
className="
rounded-md
bg-primary text-primary-foreground
hover:bg-accent hover:text-accent-foreground
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
disabled:bg-muted disabled:text-muted-foreground disabled:cursor-not-allowed
"
>
Save changes
</button>
Chart Theming
Prefer chart aliases for status intent and chart scale tokens for multi-series data.
const seriesColors = {
revenue: "var(--chart-1)",
cost: "var(--chart-2)",
margin: "var(--chart-3)",
positive: "var(--chart-positive)",
negative: "var(--chart-negative)",
}
const seriesColors = {
revenue: "var(--chart-1)",
cost: "var(--chart-2)",
margin: "var(--chart-3)",
positive: "var(--chart-positive)",
negative: "var(--chart-negative)",
}
Accessibility Checklist
- Ensure text pairs meet WCAG contrast (minimum 4.5:1 for body text).
- Never remove focus ring; map it to
--ring. - Validate light and dark variants for all status tokens.
- Use semantic tokens for disabled states and preserve readability.
Troubleshooting
Dark mode not changing: ensure ThemeProvider uses attribute="class" and your root gets .dark.
Token has no effect: check it exists in both :root and .dark and is mapped in @theme inline.
Component looks off-brand: replace hardcoded hex with semantic tokens or utilities.
Migration from Hex
Move custom CSS from fixed hex values to semantic tokens to keep themes portable.
/* Before */
.cta {
background: #232122;
color: #ffffff;
border: 1px solid #e4e4e7;
}
/* After */
.cta {
background: var(--primary);
color: var(--primary-foreground);
border: 1px solid var(--border);
}
/* Before */
.cta {
background: #232122;
color: #ffffff;
border: 1px solid #e4e4e7;
}
/* After */
.cta {
background: var(--primary);
color: var(--primary-foreground);
border: 1px solid var(--border);
}