Accent colors

View on GitHub

Add a new accent color

Worked example: adding an 11th option, teal. Because AccentColor is 'default' | ProjectColors (see the READMEAccent colorsThe accent color is the one brand color used for buttons, links, selected states, and highlights across the whole interface — chosen once per user (Account…'s architecture section), this single new name becomes both a new accent option and a new project badge color at the same time — there's no way to add one without the other, since they're the same union. Four separate, hand-maintained places need the new name; miss one and you get a different specific kind of breakage, not a build error.

Step 1 — Add it to the shared color vocabulary

File: resources/js/types/Projects.ts

export type ProjectColors =
    | 'red'
    | 'orange'
    | 'yellow'
    | 'green'
    | 'lime'
    | 'blue'
    | 'sky'
    | 'violet'
    | 'purple'
    | 'pink'
    | 'teal';

export const AVAILABLE_COLORS: ProjectColors[] = [
    'red',
    'orange',
    'yellow',
    'green',
    'lime',
    'blue',
    'sky',
    'violet',
    'purple',
    'pink',
    'teal',
];

This alone makes teal a valid project badge color end to end — every project color picker (NewProjectModal, ProjectDetailsForm, ProjectOnboardingForm, and others) reads from AVAILABLE_COLORS generically, no per-component change needed. It does not yet make teal render correctly (step 2) or make it selectable as an accent (steps 3–4) — each of those needs its own update below.

The name must be a real Tailwind color name (teal, not something invented) — step 2's classes are literal strings Tailwind's build scans for; a made-up name has no bg-‹name›-500 utility to generate at all.

Step 2 — Give it a project-badge Tailwind theme

File: resources/js/utils/colors.ts

teal: {
    badgeBg: 'bg-teal-500/10 text-teal-400',
    border: 'hover:border-teal-500/30 shadow-teal-500/5',
    gradient: 'from-teal-500/5 to-transparent',
    accent: 'bg-teal-500',
    textGroupHover: 'group-hover:text-teal-500',
},

These must be written out literally, not built from a template — Tailwind's scanner only generates a utility class if the exact string appears somewhere in the scanned source (tailwind.config.js's content globs include resources/js/**/*.{ts,tsx}); a `bg-${color}-500` template expression is invisible to that scan and would silently produce no CSS at all, no matter how correct the runtime string ends up being. Copy the five-property shape from an existing color exactly, swapping only the color name.

Step 3 — Nothing to do: the accent guard follows the map

isAccentColor() (resources/js/utils/accentColors.ts), which AccentContext.tsx uses to validate the value read back from localStorage, is derived from PROJECT_ACCENT_HEX (step 4) plus 'default' and custom hex colors, so there is no list of names to edit here. Adding teal to PROJECT_ACCENT_HEX is what makes a stored teal survive a page reload.

Step 4 — Give it accent hex values and a place in the picker

File: resources/js/utils/accentColors.ts

export const ACCENT_COLOR_OPTIONS: AccentColor[] = [
    'default',
    'red',
    'orange',
    'yellow',
    'green',
    'lime',
    'blue',
    'sky',
    'violet',
    'purple',
    'pink',
    'teal',
];

const PROJECT_ACCENT_HEX: Record<
    ProjectColors,
    { base: string; light: string }
> = {
    red: { base: '#ef4444', light: '#f87171' },
    orange: { base: '#f97316', light: '#fb923c' },
    yellow: { base: '#eab308', light: '#facc15' },
    green: { base: '#22c55e', light: '#4ade80' },
    lime: { base: '#84cc16', light: '#a3e635' },
    blue: { base: '#3b82f6', light: '#60a5fa' },
    sky: { base: '#0ea5e9', light: '#38bdf8' },
    violet: { base: '#8b5cf6', light: '#a78bfa' },
    purple: { base: '#a855f7', light: '#c084fc' },
    pink: { base: '#ec4899', light: '#f472b6' },
    teal: { base: '#14b8a6', light: '#2dd4bf' },
};

base/light here are hex codes, not Tailwind class names — this is the map step 2 does not share, even though both are keyed by the same teal name (see the README's architecture note). Use the official Tailwind palette's -500/-400 hex values for the color (here, teal-500/teal-400) so the accent swatch and the project badge read as "the same teal," even though nothing enforces that match in code.

ACCENT_COLOR_OPTIONS is what the Preferences accent picker draws: ACCENT_PALETTE is built from it with getAccentSwatch() and handed to the shared ColorPicker, which shows each option as a dot. There is no label map to extend any more, so unlike the old swatch list the compiler will not warn you when you forget the new name here — add it to ACCENT_COLOR_OPTIONS by hand.

Step 5 — Update the pre-paint script (don't skip this one)

File: resources/views/app.blade.php

There's a fourth, independent copy of the base/light hex pair, inside the inline <script> in <head> that applies the stored theme/accent before React ever mounts (to avoid a flash of the wrong color on first paint — see ../theme-colors/01-how-theme-switching-works.mdTheme colors (dark / light)How theme switching worksRead this before touching anything in this category — the other guides assume you know how theme, resolvedTheme, and data-theme relate. for the same "before first paint" concern on the theme side). This script can't import from accentColors.ts — it runs before any bundle does — so it hardcodes its own copy in plain JS:

<script>
    (function () {
        try {
            var stored = localStorage.getItem('theme');
            var mode = (stored === 'light' || stored === 'dark' || stored === 'system') ? stored : 'dark';
            var resolved = mode === 'system'
                ? (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark')
                : mode;
            document.documentElement.setAttribute('data-theme', resolved);

            var accentHex = {
                red: ['#ef4444', '#f87171'],
                orange: ['#f97316', '#fb923c'],
                yellow: ['#eab308', '#facc15'],
                green: ['#22c55e', '#4ade80'],
                lime: ['#84cc16', '#a3e635'],
                blue: ['#3b82f6', '#60a5fa'],
                sky: ['#0ea5e9', '#38bdf8'],
                violet: ['#8b5cf6', '#a78bfa'],
                purple: ['#a855f7', '#c084fc'],
                pink: ['#ec4899', '#f472b6'],
                teal: ['#14b8a6', '#2dd4bf']
            };
            var accent = localStorage.getItem('accentColor');
            if (accent && accent !== 'default' && accentHex[accent]) {
                var base = accentHex[accent][0];
                var light = accentHex[accent][1];
                var r = parseInt(base.slice(1, 3), 16);
                var g = parseInt(base.slice(3, 5), 16);
                var b = parseInt(base.slice(5, 7), 16);
                var alpha = resolved === 'light' ? 0.12 : 0.2;
                var root = document.documentElement.style;
                root.setProperty('--accent-color', base);
                root.setProperty('--accent-light-color', light);
                root.setProperty('--accent-color-opacity', 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')');
            }
        } catch (e) {}
    })();
</script>

The script also has a branch for a custom hex accent (it derives the light shade the same way lighten() does); a new preset name does not touch it.

Skip this step and teal still works correctly — but only after AccentProvider's effect runs on mount. Until then, the very first paint renders with no accent override at all (the plain --accent-color global.css defines), then visibly snaps to teal a moment later — the exact flash-of-wrong-color this script exists to prevent for every other accent. Keep this hex pair in sync with PROJECT_ACCENT_HEX in step 4 by hand; nothing enforces the two match.

Tests

  • resources/js/utils/colors.test.ts — its test.each(AVAILABLE_COLORS)('returns a complete, color-matched theme for "%s"', ...) already iterates the array generically; once step 1 adds 'teal' and step 2 adds its entry, this test covers it with zero changes, as long as step 2's classes follow the exact bg-${color}-500 naming pattern the test asserts against.
  • resources/js/utils/accentColors.test.ts already exists — extend it with a case covering getAccentSwatch('teal') (returns the base hex) and getAccentCssVariables('teal', 'dark') /getAccentCssVariables('teal', 'light') (returns the right accentColor/accentLightColor, and an accentColorOpacity alpha of 0.2 for dark vs. 0.12 for light), mirroring resources/js/context/AccentContext.test.tsx's existing assertions on getAccentCssVariables's shape for the colors it already exercises.
  • resources/js/context/AccentContext.test.tsx — no change required; its 'setAccentColor updates state, persists to localStorage, and sets the CSS variables' test uses a fixed existing color as its fixture and isn't parameterized over every AccentColor, so a new name doesn't need a new case here unless you want one for extra confidence.