Theming

Using CSS variables and theme tokens.

Want to build your theme visually? Use buridan/create to preview colors, radius, and fonts, then generate a preset for your project.

We use and recommend CSS variables for theming. This approach provides semantic design tokens such as background, foreground, and primary, which components rely on by default. You can customize the appearance of your application by overriding these tokens in your CSS, without needing to modify component-level classes.

rx.el.div(class_name="bg-background text-foreground")

Tailwind V4

We recommend using Tailwind V4 in your Reflex application as it allows for better syntax use nad avoids duplicating codes. Using Tailwind V4 also allows us to bypass extending rxconfig.py with corresponding CSS tokens by utilizing @theme inline syntax.

A basic Tailwind V4 setup in Reflex could look like the following:

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
}

:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
}

By using @theme inline we now have access to those tokens directly inside any component.

However, we some Tailwind packages get processed as JavaScript files, for example @tailwind/typography, and these kinds of packages need to be added into rxconfig.py directly.

import reflex as rx
from reflex.plugins.shared_tailwind import TailwindConfig

config = rx.Config(
    ...,
    plugins=[
        rx.plugins.TailwindV4Plugin(
            config=TailwindConfig(plugins=["@tailwindcss/typography"])
        ),
    ],
)

Finally, make sure to pull your CSS files where your tokens are defined in your rx.App instance.

app = rx.App(stylesheets=["globals.css"])

Tailwind maps these tokens into utilities like bg-background, text-foreground, border-border, and rounded-lg. Dark mode works by overriding the same tokens inside a .dark selector.

Theme Tokens

These tokens live in your CSS file under :root and .dark.

Token What it controls Used by
background / foreground The default app background and text color. The page shell, page sections, and default text.
card / card-foreground Elevated surfaces and the content inside them. Card, dashboard panels, settings panels.
popover / popover-foreground Floating surfaces and the content inside them. Popover, DropdownMenu, ContextMenu, and other overlays.
primary / primary-foreground High-emphasis actions and brand surfaces. Default Button, selected states, badges, and active accents.
secondary / secondary-foreground Lower-emphasis filled actions and supporting surfaces. Secondary buttons, secondary badges, and supporting UI.
muted / muted-foreground Subtle surfaces and lower-emphasis content. Descriptions, placeholders, empty states, helper text, and subdued surfaces.
accent / accent-foreground Interactive hover, focus, and active surfaces. Ghost buttons, menu highlight states, hovered rows, and selected items.
destructive Destructive actions and error emphasis. Destructive buttons, invalid states, and destructive menu items.
border Default borders and separators. Cards, menus, tables, separators, and layout dividers.
input Form control borders and input surface treatment. Input, Textarea, Select, and outline-style controls.
ring Focus rings and outlines. Buttons, inputs, checkboxes, menus, and other focusable controls.
chart-1 ... chart-5 The default chart palette. Charts and chart-driven dashboard blocks.
sidebar / sidebar-foreground The base sidebar surface and default sidebar text. The Sidebar container and its default content.
sidebar-primary / sidebar-primary-foreground High-emphasis actions inside the sidebar. Active items, icon tiles, badges, and sidebar CTAs.
sidebar-accent / sidebar-accent-foreground Hover and selected states inside the sidebar. Sidebar menu hover states, open items, and interactive rows.
sidebar-border Sidebar-specific borders and separators. Sidebar headers, groups, and internal dividers.
sidebar-ring Sidebar-specific focus rings. Focused controls inside the sidebar.
radius The base corner radius scale. Cards, inputs, buttons, popovers, and the derived radius-* tokens.

Adding New Tokens

To add a new token, define it under :root and .dark, then expose it to Tailwind by appending rxconfig.py.

:root {
  --warning: oklch(0.84 0.16 84);
  --warning-foreground: oklch(0.28 0.07 46);
}

.dark {
  --warning: oklch(0.41 0.11 46);
  --warning-foreground: oklch(0.99 0.02 95);
}

You can now use bg-warning and text-warning-foreground in your components.

rx.el.div(class_name="bg-warning text-warning-foreground")

Default Theme CSS

The following is the full default neutral theme scaffold. Copy it into your global CSS file and adjust the tokens as needed.

This scaffold also corresponds to preset b0 in buridan/create.

@custom-variant dark (&:is(.dark *));

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --font-sans: var(--font-family);
  --font-heading: var(--font-family);
  --font-mono: var(--font-family);
  --color-sidebar-ring: var(--sidebar-ring);
  --color-sidebar-border: var(--sidebar-border);
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
  --color-sidebar-accent: var(--sidebar-accent);
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
  --color-sidebar-primary: var(--sidebar-primary);
  --color-sidebar-foreground: var(--sidebar-foreground);
  --color-sidebar: var(--sidebar);
  --color-chart-5: var(--chart-5);
  --color-chart-4: var(--chart-4);
  --color-chart-3: var(--chart-3);
  --color-chart-2: var(--chart-2);
  --color-chart-1: var(--chart-1);
  --color-ring: var(--ring);
  --color-input: var(--input);
  --color-border: var(--border);
  --color-destructive: var(--destructive);
  --color-accent-foreground: var(--accent-foreground);
  --color-accent: var(--accent);
  --color-muted-foreground: var(--muted-foreground);
  --color-muted: var(--muted);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-secondary: var(--secondary);
  --color-primary-foreground: var(--primary-foreground);
  --color-primary: var(--primary);
  --color-popover-foreground: var(--popover-foreground);
  --color-popover: var(--popover);
  --color-card-foreground: var(--card-foreground);
  --color-card: var(--card);
  --radius-sm: calc(var(--radius) * 0.6);
  --radius-md: calc(var(--radius) * 0.8);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) * 1.4);
  --radius-2xl: calc(var(--radius) * 1.8);
  --radius-3xl: calc(var(--radius) * 2.2);
  --radius-4xl: calc(var(--radius) * 2.6);
}

:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --chart-1: oklch(0.87 0 0);
  --chart-2: oklch(0.556 0 0);
  --chart-3: oklch(0.439 0 0);
  --chart-4: oklch(0.371 0 0);
  --chart-5: oklch(0.269 0 0);
  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);
  --sidebar-primary: oklch(0.205 0 0);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.205 0 0);
  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: oklch(0.708 0 0);
  --radius: 0.5rem;
  --border-width: 1px;
  --card-padding: 1.25rem;
  --card-gap: 1rem;
  --font-family: "Inter", sans-serif;
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.205 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);
  --chart-1: oklch(0.87 0 0);
  --chart-2: oklch(0.556 0 0);
  --chart-3: oklch(0.439 0 0);
  --chart-4: oklch(0.371 0 0);
  --chart-5: oklch(0.269 0 0);
  --sidebar: oklch(0.205 0 0);
  --sidebar-foreground: oklch(0.985 0 0);
  --sidebar-primary: oklch(0.488 0.243 264.376);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.269 0 0);
  --sidebar-accent-foreground: oklch(0.985 0 0);
  --sidebar-border: oklch(1 0 0 / 10%);
  --sidebar-ring: oklch(0.556 0 0);
  --radius: 0.5rem;
  --border-width: 1px;
  --card-padding: 1.25rem;
  --card-gap: 1rem;
  --font-family: "Inter", sans-serif;
}