Skip to main content
The WhatsApp Chat application uses a comprehensive theming system built on CSS custom properties with OKLCH color space for perceptually uniform colors across light and dark modes.

Color palette

The theme is defined using CSS variables in app/globals.css:46-115. The application features a WhatsApp-inspired green color palette with carefully crafted color tokens.

Primary colors

The primary color uses a green hue (145° in OKLCH) that matches WhatsApp’s branding:
app/globals.css
:root {
  --primary: oklch(0.45 0.15 145);
  --primary-foreground: oklch(1 0 0);
  --accent: oklch(0.55 0.18 145);
  --accent-foreground: oklch(1 0 0);
}

.dark {
  --primary: oklch(0.6 0.18 145);
  --primary-foreground: oklch(0.1 0.01 140);
  --accent: oklch(0.68 0.2 145);
  --accent-foreground: oklch(0.16 0.02 145);
}
OKLCH (Lightness, Chroma, Hue) provides better perceptual uniformity than RGB or HSL, ensuring colors appear consistent across different lightness levels.

Message bubble colors

Custom color tokens define the appearance of outgoing and incoming message bubbles:
--bubble-outgoing: oklch(0.92 0.04 145);
--bubble-incoming: oklch(0.995 0.015 95);
These are used in the MessageBubble component:
components/chat/message-bubble.tsx
const bubbleClasses = cn(
  "relative rounded-3xl px-4 py-2 shadow-sm transition-colors",
  isOutgoing
    ? "rounded-br-lg bg-bubble-outgoing text-foreground"
    : "rounded-bl-lg bg-bubble-incoming text-foreground"
)

Message status colors

Status indicators use distinct colors to show message delivery states:
app/globals.css
:root {
  --status-sent: oklch(0.65 0.02 140);
  --status-delivered: oklch(0.55 0.18 145);
  --status-read: oklch(0.6 0.2 210);
}

.dark {
  --status-sent: oklch(0.72 0.02 140);
  --status-delivered: oklch(0.68 0.2 145);
  --status-read: oklch(0.72 0.2 220);
}
Implementation in the status icon component:
components/chat/message-bubble.tsx
const statusColor: Record<Message["status"], string> = {
  queued: "text-muted-foreground",
  sending: "text-muted-foreground",
  sent: "text-muted-foreground",
  delivered: "text-status-delivered",
  read: "text-status-read",
  error: "text-destructive",
}

Theme configuration

The theme system uses Tailwind CSS v4’s @theme directive to expose CSS variables as Tailwind utilities.

Defining theme tokens

app/globals.css
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-bubble-outgoing: var(--bubble-outgoing);
  --color-bubble-incoming: var(--bubble-incoming);
  --color-status-delivered: var(--status-delivered);
  --color-status-read: var(--status-read);
  /* ... */
}
All CSS variables prefixed with --color- become available as Tailwind color utilities like bg-bubble-outgoing or text-status-read.

Radius tokens

Border radius values are calculated relative to a base radius:
app/globals.css
@theme inline {
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
}

:root {
  --radius: 0.75rem;
}

Dark mode support

The application supports automatic dark mode detection through the suppressHydrationWarning attribute:
app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={`${inter.variable} font-sans antialiased bg-app-surface`}>
        {children}
      </body>
    </html>
  )
}

Dark variant

The dark mode is controlled using a custom variant defined in the CSS:
app/globals.css
@custom-variant dark (&:is(.dark *));
This allows dark mode styles to cascade properly when the .dark class is applied to a parent element.

Customizing colors

To customize the color palette, modify the CSS variables in app/globals.css:
  1. Change the primary hue: Adjust the third parameter (hue) in OKLCH values:
/* Change from green (145) to blue (220) */
--primary: oklch(0.45 0.15 220);
--accent: oklch(0.55 0.18 220);
  1. Adjust lightness: Modify the first parameter for lighter or darker colors:
/* Make bubbles darker */
--bubble-outgoing: oklch(0.85 0.04 145); /* was 0.92 */
  1. Increase saturation: Change the second parameter (chroma) for more vivid colors:
/* More saturated accent color */
--accent: oklch(0.55 0.25 145); /* was 0.18 */
Keep lightness values above 0.4 for light mode and below 0.7 for dark mode to maintain readability.
The sidebar has dedicated color tokens for independent styling:
app/globals.css
:root {
  --sidebar: oklch(0.992 0.004 140);
  --sidebar-foreground: oklch(0.25 0.02 140);
  --sidebar-primary: oklch(0.48 0.17 145);
  --sidebar-accent: oklch(0.96 0.05 145);
  --sidebar-border: oklch(0.9 0.01 140);
}
Used throughout the sidebar component:
components/chat/sidebar.tsx
<aside className="flex h-full w-full flex-col border-r border-sidebar-border/70 bg-sidebar/80 backdrop-blur-xl">
  <Button
    variant="outline"
    className="border-sidebar-border/80 bg-sidebar"
  >
    <Plus className="h-5 w-5" />
  </Button>
</aside>

Build docs developers (and LLMs) love