Overview

Understanding the Apsara theming system and ThemeProvider.

Apsara provides a theming system built on CSS custom properties (tokens). Tokens are semantic variables that automatically resolve to appropriate values based on the active theme—so your UI adapts seamlessly when users switch between light and dark modes or when you change accent colors, without any code changes.

Installation

Wrap your application with the ThemeProvider component:

1import { ThemeProvider } from "@raystack/apsara";
2
3function App() {
4 return (
5 <ThemeProvider defaultTheme="system">
6 <YourApp />
7 </ThemeProvider>
8 );
9}

Customization

The ThemeProvider accepts props to control the visual identity of your application. Combine style variants with accentColor and grayColor to create distinct aesthetics—from sharp and technical to warm and editorial. The defaultTheme prop controls light/dark mode, with system respecting the user's OS preference.

1// Clean, technical aesthetic
2<ThemeProvider style="modern" accentColor="indigo" grayColor="slate">
3
4// Warm, editorial feel
5<ThemeProvider style="traditional" accentColor="orange" grayColor="mauve">
6
7// Vibrant and fresh
8<ThemeProvider style="modern" accentColor="mint" grayColor="gray">

See API Reference for all available props and options.

Tokens

Tokens follow two naming patterns:

Semantic tokens — for context-aware values that adapt to theme:

1--rs-{category}-{property}-{variant}-{state}

Scale tokens — for numerical progressions:

1--rs-{category}-{step}

Examples:

  • --rs-color-foreground-base-primary — primary text color
  • --rs-color-background-accent-emphasis — accent button background
  • --rs-space-5 — 16px spacing
  • --rs-radius-3 — medium border radius
  • --rs-shadow-lifted — elevated shadow

Using tokens in CSS:

1.custom-card {
2 background: var(--rs-color-background-base-secondary);
3 border: 1px solid var(--rs-color-border-base-primary);
4 border-radius: var(--rs-radius-4);
5 padding: var(--rs-space-5);
6 box-shadow: var(--rs-shadow-feather);
7}

Token Categories:

  • Colors — foreground, background, border, and overlay colors
  • Spacing — consistent scale from 2px to 120px
  • Radius — border radius that adapts to style variants
  • Typography — font families, sizes, weights, and line heights
  • Effects — shadows and blur for depth and elevation

API Reference

ThemeProvider

The ThemeProvider component wraps your application and manages theme state. It handles persisting the user's preference to localStorage, syncing with system preferences, and injecting the appropriate CSS variables into the document.

Prop

Type

useTheme

The useTheme hook provides access to the current theme state and methods to change it. Use this to build theme toggles, read the resolved theme for conditional rendering, or sync with external systems.

1import { useTheme } from "@raystack/apsara";
2
3function ThemeToggle() {
4 const { theme, setTheme, resolvedTheme } = useTheme();
5
6 return (
7 <button onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}>
8 Toggle theme
9 </button>
10 );
11}

Prop

Type

Framework Integration

HTML Attributes — the ThemeProvider sets data attributes on the document element for CSS targeting:

  • data-theme — current color scheme (light | dark)
  • data-style — active style variant (modern | traditional)
  • data-accent-color — active accent color (indigo | orange | mint)
  • data-gray-color — active gray variant (gray | mauve | slate)

SSR & Flash Prevention — the ThemeProvider includes an inline script that runs before React hydration to prevent flash of incorrect theme. For SSR frameworks, include the provider in your root layout:

1// Next.js App Router: app/layout.tsx
2import { ThemeProvider } from "@raystack/apsara";
3
4export default function RootLayout({ children }) {
5 return (
6 <html lang="en" suppressHydrationWarning>
7 <body>
8 <ThemeProvider>{children}</ThemeProvider>
9 </body>
10 </html>
11 );
12}

The suppressHydrationWarning is required because the theme script modifies the HTML element before React hydrates.

Scoped Theming

Themes are not limited to the document root. Any element with a data-theme attribute creates an isolated theme scope — descendants resolve every design token from the nearest scoped ancestor. This enables theme preview cards, split-screen comparisons, and dark sidebars in light apps without any extra plumbing.

Bare attribute

Because scoping is implemented in CSS, you can opt in by simply setting the attribute on any element:

1<html data-theme="dark">
2 {/* Page is dark */}
3 <div data-theme="light">
4 {/* This subtree renders with light tokens */}
5 <Button>Light button inside dark page</Button>
6 </div>
7</html>

The package's stylesheet handles the rest: every --rs-color-* token, color-scheme for native form controls and scrollbars, and the smooth transition during theme switches all follow the scoped attribute.

ThemeScope component

For a typed convenience wrapper, use ThemeScope:

1import { ThemeScope } from "@raystack/apsara";
2
3<ThemeScope theme="dark">
4 <Card>Dark scoped card</Card>
5</ThemeScope>

ThemeScope writes data-theme (and optionally data-accent-color, data-gray-color, data-style) onto a wrapper element. By default it renders a <div>. Use the render prop to fuse the scope onto an element you already have, with no extra wrapping div:

1<ThemeScope theme="dark" render={<Flex direction="column" gap={5} />}>
2 <Heading>...</Heading>
3 <Text>...</Text>
4</ThemeScope>

Combining with accent and gray overrides

A scope can override accent or gray independently of theme. This is useful for highlighting a section without changing its color scheme:

1<ThemeScope accentColor="orange">
2 <Button color="accent">Orange accent in this region only</Button>
3</ThemeScope>
4
5<ThemeScope theme="dark" accentColor="mint" grayColor="slate">
6 <Card>Dark mint-on-slate card</Card>
7</ThemeScope>

State management

ThemeScope is stateless — the consumer owns the theme value. For an interactive scope, drive it with React state:

1const [scopeTheme, setScopeTheme] = useState<'light' | 'dark'>('dark');
2
3<ThemeScope theme={scopeTheme} render={<Flex direction="column" />}>
4 <Toggle onClick={() => setScopeTheme(t => t === 'dark' ? 'light' : 'dark')}>
5 Toggle this scope
6 </Toggle>
7 <Card>...</Card>
8</ThemeScope>

If you need persistence across page loads, manage it yourself with localStorage and a useEffect. ThemeScope deliberately avoids touching storage to keep the component pure and to avoid disambiguation issues when multiple scopes share a page.

Prop

Type

When to reach for ThemeScope vs. the bare attribute

  • Use the bare data-theme attribute when you're already rendering a custom element and don't want another wrapper. The CSS handles everything — components inside will theme correctly.
  • Use ThemeScope when you want typed props (theme, accentColor, etc.), defaults handled for you, and a single import that documents intent.
  • Both produce the same DOM output when ThemeScope is given a render prop.