Theme Builder

Build a PyColors theme from your brand colors

Free, client-side, PyColors-compatible theme generation for production-shaped SaaS interfaces.

It runs locally in your browser. No account is required, and theme inputs or generated tokens are not persisted or transmitted.

Configure a theme

Values stay in this editor. No input is saved, shared, or sent to a server.

Theme inputs

Required. Enter a six-digit hexadecimal color such as #6a30d4.

Optional. Used for deterministic theme metadata.

Optional. Leave empty to derive a conservative neutral from the brand hue.

Optional. Leave empty to use the engine default light background.

Preview mode

Current preview: Light.

Scoped SaaS preview

A representative settings workspace using generated light mode values. It is contrast-aware evidence, not an accessibility certification.

Workspace active

Northstar workspace

Manage the project defaults used by your delivery team.

Pro plan
Changes saved
Project defaults are ready to use.
Review seats
Two invited teammates have not joined.
Attention needed
A sandbox integration needs reconnection.

Token exports

Copy deterministic output from the current valid generated theme. Nothing is uploaded or saved.

Load these semantic overrides after the PyColors token import.

Generated override example

:root {
  --background: #ffffff;
  --background: oklch(1.0000 0.0000 0.00);
  --foreground: #121116;
  --foreground: oklch(0.1800 0.0113 291.61);
  /* Remaining semantic roles are included in the copied export. */
}

.dark {
  --background: #060508;
  --background: oklch(0.1200 0.0075 291.61);
  --foreground: #f5f4fc;
  --foreground: oklch(0.9700 0.0113 291.61);
  /* Remaining semantic roles are included in the copied export. */
}

CSS

:root {
  --background: #ffffff;
  --background: oklch(1.0000 0.0000 0.00);
  --foreground: #121116;
  --foreground: oklch(0.1800 0.0113 291.61);
  --card: #f2f1f8;
  --card: oklch(0.9600 0.0095 291.61);
  --card-foreground: #121116;
  --card-foreground: oklch(0.1800 0.0113 291.61);
  --popover: #f2f1f8;
  --popover: oklch(0.9600 0.0095 291.61);
  --popover-foreground: #121116;
  --popover-foreground: oklch(0.1800 0.0113 291.61);
  --primary: #6a30d4;
  --primary: oklch(0.4904 0.2293 291.61);
  --primary-foreground: #fcfbff;
  --primary-foreground: oklch(0.9900 0.0051 291.61);
  --secondary: #e3e2eb;
  --secondary: oklch(0.9151 0.0120 291.61);
  --secondary-foreground: #121116;
  --secondary-foreground: oklch(0.1800 0.0113 291.61);
  --muted: #e3e2eb;
  --muted: oklch(0.9151 0.0120 291.61);
  --muted-foreground: #121116;
  --muted-foreground: oklch(0.1800 0.0113 291.61);
  --accent: #e3e0f3;
  --accent: oklch(0.9151 0.0252 291.61);
  --accent-foreground: #121116;
  --accent-foreground: oklch(0.1800 0.0113 291.61);
  --destructive: #e40016;
  --destructive: oklch(0.5770 0.2354 27.32);
  --destructive-foreground: #fafafa;
  --destructive-foreground: oklch(0.9850 0.0000 0.00);
  --success: #16a34a;
  --success: oklch(0.6271 0.1699 149.21);
  --success-foreground: #09120b;
  --success-foreground: oklch(0.1700 0.0200 152.00);
  --warning: #e5c057;
  --warning: oklch(0.8200 0.1300 90.00);
  --warning-foreground: #201a09;
  --warning-foreground: oklch(0.2200 0.0300 90.00);
  --border: #b6b4c0;
  --border: oklch(0.7752 0.0170 291.61);
  --border-subtle: #ceccd7;
  --border-subtle: oklch(0.8501 0.0145 291.61);
  --border-strong: #838190;
  --border-strong: oklch(0.6103 0.0220 291.61);
  --input: #b6b4c0;
  --input: oklch(0.7752 0.0170 291.61);
  --ring: #6a30d4;
  --ring: oklch(0.4904 0.2293 291.61);
  --surface: #fcfbff;
  --surface: oklch(0.9900 0.0051 291.61);
  --surface-foreground: #121116;
  --surface-foreground: oklch(0.1800 0.0113 291.61);
  --surface-muted: #f2f1f8;
  --surface-muted: oklch(0.9600 0.0095 291.61);
  --surface-elevated: #f2f1f8;
  --surface-elevated: oklch(0.9600 0.0095 291.61);
  --surface-inverted: #121116;
  --surface-inverted: oklch(0.1800 0.0113 291.61);
  --surface-inverted-foreground: #fcfbff;
  --surface-inverted-foreground: oklch(0.9900 0.0051 291.61);
}

.dark {
  --background: #060508;
  --background: oklch(0.1200 0.0075 291.61);
  --foreground: #f5f4fc;
  --foreground: oklch(0.9700 0.0113 291.61);
  --card: #0d0c10;
  --card: oklch(0.1570 0.0095 291.61);
  --card-foreground: #f5f4fc;
  --card-foreground: oklch(0.9700 0.0113 291.61);
  --popover: #0d0c10;
  --popover: oklch(0.1570 0.0095 291.61);
  --popover-foreground: #f5f4fc;
  --popover-foreground: oklch(0.9700 0.0113 291.61);
  --primary: #6a30d4;
  --primary: oklch(0.4904 0.2293 291.61);
  --primary-foreground: #f5f4fc;
  --primary-foreground: oklch(0.9700 0.0113 291.61);
  --secondary: #17161c;
  --secondary: oklch(0.2052 0.0120 291.61);
  --secondary-foreground: #f5f4fc;
  --secondary-foreground: oklch(0.9700 0.0113 291.61);
  --muted: #17161c;
  --muted: oklch(0.2052 0.0120 291.61);
  --muted-foreground: #f5f4fc;
  --muted-foreground: oklch(0.9700 0.0113 291.61);
  --accent: #1a093b;
  --accent: oklch(0.2052 0.0894 291.61);
  --accent-foreground: #f5f4fc;
  --accent-foreground: oklch(0.9700 0.0113 291.61);
  --destructive: #ff6568;
  --destructive: oklch(0.7040 0.1877 22.22);
  --destructive-foreground: #fafafa;
  --destructive-foreground: oklch(0.9850 0.0000 0.00);
  --success: #22c55e;
  --success: oklch(0.7227 0.1920 149.58);
  --success-foreground: #040b06;
  --success-foreground: oklch(0.1400 0.0200 152.00);
  --warning: #e5c057;
  --warning: oklch(0.8200 0.1300 90.00);
  --warning-foreground: #201a09;
  --warning-foreground: oklch(0.2200 0.0300 90.00);
  --border: #35333d;
  --border: oklch(0.3274 0.0170 291.61);
  --border-subtle: #25242c;
  --border-subtle: oklch(0.2645 0.0145 291.61);
  --border-strong: #514f5d;
  --border-strong: oklch(0.4349 0.0220 291.61);
  --input: #35333d;
  --input: oklch(0.3274 0.0170 291.61);
  --ring: #6a30d4;
  --ring: oklch(0.4904 0.2293 291.61);
  --surface: #060508;
  --surface: oklch(0.1200 0.0075 291.61);
  --surface-foreground: #f5f4fc;
  --surface-foreground: oklch(0.9700 0.0113 291.61);
  --surface-muted: #0d0c10;
  --surface-muted: oklch(0.1570 0.0095 291.61);
  --surface-elevated: #0d0c10;
  --surface-elevated: oklch(0.1570 0.0095 291.61);
  --surface-inverted: #f5f4fc;
  --surface-inverted: oklch(0.9700 0.0113 291.61);
  --surface-inverted-foreground: #060508;
  --surface-inverted-foreground: oklch(0.1200 0.0075 291.61);
}

Continue with generated tokens

Copy the current output, then follow the supported PyColors integration path.

Contrast-aware notices

Generated evidence helps guide review; it is not an accessibility certification.

6 contrast checks remain below target.

  • neutral derived
    No neutral color was supplied, so the neutral scale was derived from the brand hue at conservative chroma.
  • gamut mapped
    One or more generated light-mode scale colors were mapped into sRGB by reducing chroma while preserving lightness and hue.
  • foreground fallback used
    A higher-contrast foreground candidate was selected for primary-foreground in light mode.
  • foreground fallback used
    A higher-contrast foreground candidate was selected for surface-inverted-foreground in light mode.
  • gamut mapped
    One or more generated dark-mode scale colors were mapped into sRGB by reducing chroma while preserving lightness and hue.
  • foreground fallback used
    A higher-contrast foreground candidate was selected for surface-inverted-foreground in dark mode.
  • Contrast warning
    border on background measures 2.04:1, below the 3:1 non-text target in light mode. Review the theme in context.
  • Contrast warning
    input on background measures 2.04:1, below the 3:1 non-text target in light mode. Review the theme in context.
  • Contrast warning
    destructive-foreground on destructive measures 2.75:1, below the 4.5:1 normal-text target in dark mode. Review the theme in context.
  • Contrast warning
    ring on background measures 2.89:1, below the 3:1 non-text target in dark mode. Review the theme in context.
  • Contrast warning
    border on background measures 1.64:1, below the 3:1 non-text target in dark mode. Review the theme in context.
  • Contrast warning
    input on background measures 1.64:1, below the 3:1 non-text target in dark mode. Review the theme in context.

Apply generated overrides in the supported order

  1. Install and use @pycolors/tokens.
  2. Import @pycolors/tokens/tokens.css once in your global stylesheet.
  3. Place the generated CSS overrides after that token import.
  4. Keep application code on semantic utilities and public @pycolors/ui imports.
  5. Verify the complete interface in both light and dark modes.

The generated overrides use the existing Tailwind v4 @theme inline bridge in the token package; they do not create a second Tailwind theme contract. Read the theming guide for the supported integration boundary.

Review the generated theme in context

Theme Builder is contrast-aware, not an accessibility certification. Review generated values in the real interface before shipping.

  • Brand perception across your product and audience.
  • Complete component and page context.
  • Color-vision deficiencies and non-color cues.
  • States communicated only by color.
  • Real focus, hover, and disabled contexts.