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.

reflex

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

To use CSS variables for theming, make the following changes to your rxconfig.py file found at the root of your Reflex app.

Import the tailwind plugin TailwindConfig, add it to the plugins=[...] list, define (or remove if unecessary) darkMode or specific plugins, then finally set the theme and extend the colors based on the CSS tokens in your globals.css file in your assets folder.

rxconfig.py

from reflex.plugins.shared_tailwind import TailwindConfig

config = rx.Config(
    plugins=[
        rx.plugins.TailwindV4Plugin(
            TailwindConfig(
                darkMode="class",
                plugins=[
                    "@tailwindcss/typography",
                    "tailwind-scrollbar",
                    "tailwindcss-animate",
                ],
                theme={
                    "extend": {
                        "colors": {
                            "background": "var(--background)",
                            "foreground": "var(--foreground)",
                        },
                    }
                },
            )
        ),
    ]
)

Further extensions are possible, such as fontFamily, borderRadius, and boxShadow by further extending the theme dictionary as such, making sure each var(...) token is defined in a CSS file.

rxconfig.py

theme={
    "extend": {
        "colors": {...},
        "fontFamily": {
            "theme": "var(--font-family)",
        },
        "borderRadius": {
            "radius": "var(--radius)",
        },
        "boxShadow": {
            "default": "var(--shadow)",
        },
    }
},

Make sure to pull your CSS files where your tokens are defined in your rx.App instance.

rx.App

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

Tailwind maps these tokens into utilities like bg-background, text-foreground, border-border, and rounded-radius. 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.

TokenWhat it controlsUsed by
background / foregroundThe default app background and text color.The page shell, page sections, and default text.
card / card-foregroundElevated surfaces and the content inside them.Card, dashboard panels, settings panels.
popover / popover-foregroundFloating surfaces and the content inside them.Popover, DropdownMenu, ContextMenu, and other overlays.
primary / primary-foregroundHigh-emphasis actions and brand surfaces.Default Button, selected states, badges, and active accents.
secondary / secondary-foregroundLower-emphasis filled actions and supporting surfaces.Secondary buttons, secondary badges, and supporting UI.
muted / muted-foregroundSubtle surfaces and lower-emphasis content.Descriptions, placeholders, empty states, helper text, and subdued surfaces.
accent / accent-foregroundInteractive hover, focus, and active surfaces.Ghost buttons, menu highlight states, hovered rows, and selected items.
destructiveDestructive actions and error emphasis.Destructive buttons, invalid states, and destructive menu items.
borderDefault borders and separators.Cards, menus, tables, separators, and layout dividers.
inputForm control borders and input surface treatment.Input, Textarea, Select, and outline-style controls.
ringFocus rings and outlines.Buttons, inputs, checkboxes, menus, and other focusable controls.
chart-1 ... chart-5The default chart palette.Charts and chart-driven dashboard blocks.
sidebar / sidebar-foregroundThe base sidebar surface and default sidebar text.The Sidebar container and its default content.
sidebar-primary / sidebar-primary-foregroundHigh-emphasis actions inside the sidebar.Active items, icon tiles, badges, and sidebar CTAs.
sidebar-accent / sidebar-accent-foregroundHover and selected states inside the sidebar.Sidebar menu hover states, open items, and interactive rows.
sidebar-borderSidebar-specific borders and separators.Sidebar headers, groups, and internal dividers.
sidebar-ringSidebar-specific focus rings.Focused controls inside the sidebar.
radiusThe 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.

assets/globals.css

: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.

reflex

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.

assets/globals.css

@tailwind base;
@tailwind components;
@tailwind utilities;

: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.625rem;
    --shadow: 0 1px 3px rgba(0,0,0,0.08);
    --border-width: 1px;
    --card-padding: 1.25rem;
    --card-gap: 1rem;
}

.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.625rem;
    --shadow: 0 1px 3px rgba(0,0,0,0.08);
    --border-width: 1px;
    --card-padding: 1.25rem;
    --card-gap: 1rem;
}