Documentation

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'

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>
  )
}

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;
  }
}

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);
}

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>

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)",
}

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);
}