Theming

Load PyColors tokens, understand their Tailwind v4 mappings, and keep UI primitives consistent across light and dark themes.

UIUpdated November 2, 2026
UITheming

Give every primitive the same semantic themeLink to section

@pycolors/ui primitives use semantic roles such as background, card, primary, border, and ring. Load those roles once with @pycolors/tokens, then let components consume them through Tailwind utilities.

Core idea

The token package owns theme values and Tailwind mappings. UI primitives consume the resulting semantic utilities. Your application owns when the dark class is applied.

Install and load the tokensLink to section

Install the UI and token packages if they are not already dependencies of your app.

pnpm add @pycolors/ui @pycolors/tokens

Import the public token stylesheet once from your global stylesheet. In a Tailwind CSS v4 app, keep both imports at the top of that stylesheet.

app/globals.css
@import "tailwindcss";
@import "@pycolors/tokens/tokens.css";

Do not copy tokens.css into your application or import a path inside the package. The supported public stylesheet is @pycolors/tokens/tokens.css.

Understand the token chainLink to section

The token stylesheet connects four layers:

LayerCard exampleResponsibility
Semantic CSS variable--cardDefines the current theme value for a card surface.
Tailwind v4 mapping--color-card: var(--card)Exposes the role to Tailwind through @theme inline.
Semantic utilitybg-cardLets application and component code request that role.
UI primitiveCardUses the shared role without owning a theme value.

The package includes mappings like these:

@pycolors/tokens/tokens.css
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  --color-card-foreground: var(--card-foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-border: var(--border);
  --color-ring: var(--ring);
}

Tailwind v4 turns those mappings into utilities such as bg-background, text-foreground, bg-card, text-card-foreground, bg-primary, border-border, and ring-ring. Changing a theme value therefore does not require rewriting component classes.

Use semantic roles in product UILink to section

Import primitives from the public @pycolors/ui entry point and keep local composition token-driven.

account-summary.tsx
import {
  Button,
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@pycolors/ui";

export function AccountSummary() {
  return (
    <Card className="border-border bg-card text-card-foreground">
      <CardHeader>
        <CardTitle>Workspace theme</CardTitle>
        <CardDescription>
          Components follow the semantic theme selected by your app.
        </CardDescription>
      </CardHeader>
      <CardContent>
        <p className="text-sm text-muted-foreground">
          Surfaces, text, borders, and focus rings update together.
        </p>
      </CardContent>
      <CardFooter>
        <Button>Save preference</Button>
      </CardFooter>
    </Card>
  );
}

The example uses semantic utilities rather than raw color values. The same component remains valid in both themes.

Generate a semantic starting pointLink to section

PyColors Theme Builder generates a client-side, contrast-aware starting set of the same semantic CSS variables. Use it when you want to begin from a brand color without creating a new token contract.

Keep the integration order unchanged:

  1. import @pycolors/tokens/tokens.css once;
  2. place the generated overrides after that import;
  3. use semantic utilities and public @pycolors/ui imports; and
  4. verify the complete interface in both light and dark modes.

The generated overrides work with this package's existing @theme inline bridge. They do not define a second Tailwind theme. Theme Builder output is contrast-aware rather than an accessibility certification, so review brand perception, focus, hover, disabled, color-vision, and complete page contexts before shipping.

Understand the color checksLink to section

Open Color readability to see which text and controls may be difficult to read or identify. Results are grouped into Light mode and Dark mode, with examples of the actual generated colors and a suggested adjustment for each check that needs review.

Technical details lists the affected CSS variables, measured contrast, target ratio, and automatic color adjustments. For precise fixes, adjust those variables after exporting your theme. These checks cover generated colors; review the complete interface and its interactive states before shipping. Borders that are needed to identify controls deserve particular attention; decorative dividers can remain subtle. See the W3C guidance on control boundaries.

Preview typography with your colorsLink to section

Use the preview tabs to explore the same generated theme in three ways:

  • Dashboard shows a complete example workspace.
  • Components lets you try forms, buttons, checkboxes, feedback, and typography.
  • Palette displays 12 accent and 12 neutral shades for the selected mode. Select a shade or named color role to inspect and copy its HEX or OKLCH value.

The Light background field controls the light-mode page background. Clear it to let the engine generate that value. The dark-mode background remains generated. Color values shown in the palette come directly from the same engine result as the exports; named roles under Colors by purpose show how those colors are used in your theme.

Theme Builder includes the same ten color presets as Starter Pro, with Monochrome and PyColors first, and a Font family selector for Geist, System, Geist Mono, Mona Sans, and Plus Jakarta Sans. Use Full screen to inspect the dashboard with a wider navigation sidebar. Your selected font, light/dark mode, activity period, and project filters remain selected when you close the preview. Dashboard content is demo data.

The site's PyColors / Monochrome selector uses the same color values as Starter Pro across marketing pages, the blog and documentation. Light, dark and system appearance remain independent of your palette choice. The other presets are available in Theme Builder and the Starter Pro demo. Preview settings stay inside the preview and do not change the site's appearance.

The selected preview tab, palette selection, and component demo controls also survive opening and closing full screen. Demo form values are only for exploring components; configuration files save theme settings, not demo content.

Use Copy typography CSS or Download typography CSS in the export section to save the font configuration separately from the CSS, Tailwind v4, or JSON color output. For a bundled font, download the provided asset and license, save it at the indicated public/fonts path, and place the typography CSS after your token imports. System needs no font file and uses the fonts available on each visitor's device. Selecting a font does not change the color tokens or their contrast results.

Download and reopen your themeLink to section

In Export your theme, choose CSS, Tailwind v4, or JSON, then use Download to save the displayed format. Color files include both light and dark modes. Typography stays in a separate CSS file.

Use Save configuration to download a .pycolors-theme.json file containing your color inputs, theme name, preset, font, radius, preview mode and dashboard settings. Later, use Open configuration to restore those settings and regenerate the theme. New files use version 2; existing version 1 files remain readable, including legacy System Sans/System Mono font identifiers. Configuration files are checked before applying them; an invalid or unsupported file leaves your current settings unchanged. The JSON color-token export is a different format and cannot be reopened as an editable configuration.

Files are processed locally without an account or uploads. There is no automatic save across page reloads: keep the configuration file to continue later.

Shared appearance and Starter ProLink to section

Named presets use the same semantic light/dark tokens in the builder and Starter Pro. Editing a brand or neutral color creates a generated custom theme. Editing only the light background retains the preset's dark colors. Contrast evidence is recalculated for the resulting colors.

The Radius control uses None / SM / MD / LG / XL. Dashboard preview adds scale (100%, 110%, 125%), density, content width, sidebar display/style/background, header, canvas and motion. These settings affect the preview, independently of the surrounding website. Font and radius CSS is exported separately from colors.

Choose Download Starter Pro config to generate appearance.ts. Replace config/appearance.ts in your starter and restart it. In a browser with existing preferences, use Customize → Reset to restore the new product defaults. Both color modes and the selected appearance settings are included; no private package or Theme Builder runtime is needed by the buyer's application.

Apply dark mode at the app shellLink to section

@pycolors/tokens defines its dark values under a .dark selector. Add the class to the document root or app shell through your existing theme logic.

document-shell.tsx
export function DocumentShell({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className="dark">
      <body>{children}</body>
    </html>
  );
}

Remove the class for the default light theme. A real application should derive that class from its existing preference, storage, or server-rendered theme strategy.

Application responsibility

PyColors provides the .dark token values. It does not provide a theme toggle, persistence API, React provider, or flash-prevention strategy. Keep those decisions in your application.

Customize without creating theme driftLink to section

Start with the existing semantic roles. If your product needs different theme values, change them once in the application's global theme layer after the package import, and review the corresponding light and dark roles together.

Keep these boundaries explicit:

  • preserve role names such as primary, card, destructive, and ring;
  • keep component code on semantic utilities instead of palette or raw values;
  • verify text contrast, focus visibility, disabled states, and status feedback in both themes;
  • do not edit files in node_modules or import package internals;
  • do not scatter token redefinitions across components or feature stylesheets;
  • treat a new public role as a design-system decision, not a local component workaround.

Prefer

  • one global import of @pycolors/tokens/tokens.css
  • semantic utilities such as bg-card and text-muted-foreground
  • the .dark class controlled by application theme logic
  • central theme changes reviewed in light and dark mode

Avoid

  • raw color values in reusable components
  • component-local copies of semantic variables
  • private imports from the UI or token packages
  • using color alone to communicate status or errors

Verification checklistLink to section

  • The token stylesheet is imported once from its public path.
  • Tailwind v4 recognizes semantic utilities from the token mappings.
  • UI imports use the public @pycolors/ui entry point.
  • Reusable components contain no raw theme colors.
  • The app shell adds and removes .dark through existing theme logic.
  • Text, controls, focus rings, and feedback remain understandable in both themes.