Skip to content
fayeuiStatic / 0.1.0
LLMs
System / Static edition

Theme and color system

Generate complete palettes, switch schemes, scope brands, and preserve readable state colors.

Contract first

Faye UI distinguishes the token contract, theme definitions, compiled component sheets and runtime environment. Revision 3 adds source linkage for every token rather than treating a larger count as proof of completeness.

There are six built-in semantic palette families: accent, secondary, danger, success, warning and info. Each has solid, hover, active, soft, soft-hover, soft-active, readable text and paired on-color values. Neutral, selected, disabled, inverse, link, placeholder and focus roles remain separate.

The default Faye UI palette pairs muted periwinkle blue (#606fbd light / #adb9ee dark), a warm coral secondary and cool slate neutrals. Text, soft surfaces, focus rings and hover/pressed states use paired palette values to retain readable contrast in both schemes.

Build complete palettes

createBrandTheme accepts explicit light/dark seeds for each family. Font and radius options are applied in both schemes, rather than being masked by scheme overrides. Surface changes are applied before palette derivation.

Preview the documentation

Open Theme in the top navigation. Choose a primary palette, pick a custom primary color, switch between system/light/dark modes, and adjust density or text direction. Custom primary colors use the theme palette generator, including readable foregrounds, hover/pressed colors and focus colors. The documentation and its live examples share these settings. Use preset color restores the selected preset; appearance choices persist locally across navigation and reloads.

The example is a developer-tool module: Apply palettes runs authoring and validation, while ordinary production components consume precompiled CSS and a small profile. Editing just a CSS accent variable does not regenerate its paired foreground or states.

Named contexts

Additional axes declare choices and a default. Resolution order is base, scheme, density, named axes sorted by name, then matching conditions in declaration order. The compiler validates references in all reachable contexts and limits expansion to 256 complete contexts. Split independent theme assets beyond that boundary.

The example's contrast axis changes the border without duplicating every scheme. Its typography selector changes the base font on the theme boundary; semantic typography references are rebound there and recompute with it.

Honest contrast reporting

The audit tests declared opaque numeric-sRGB role pairs across contexts. Standard named colors are supported. Accepted non-sRGB CSS expressions are unresolved for numeric contrast, not marked passing. Ratios are not rounded up to pass a threshold. Gradients, images, external CSS, arbitrary conditional backgrounds and full accessibility conformance require additional review.

Scope and rendering

Compile and load the theme asset before choosing its themeId. ThemeScope carries scheme, density, direction, axes, declared variables and motion settings. Portals carry the same environment but not arbitrary ancestor CSS or container geometry.

Static/server components use emitted attributes and CSS; a client provider cannot change their already-executed logical defaults. Actual Next builds, SSR/CSR applications and hydration behavior are mandatory qualification gates. See verification for source-bound results.

Editor

The separate ThemeEditor searches and edits every contract token. It supports named-axis selection, typed drafts, explicit @token.name references, JSON import/export, compiled CSS output and context-specific reset. Editing writes a final condition for exactly the selected context so earlier density/scheme overrides cannot mask it. Palette rebuilding is explicit and applies all paired states together.

Shared interaction defaults

FayeUIProvider and ThemeScope accept interaction with the same variant, level and effects options as supported controls. Theme scopes extend their parent's declared options; an isolated scope starts from the library defaults. React portals preserve the active configuration.

Precedence is library defaults, provider/scoped defaults, component settings, then explicit instance options. Partial options preserve inherited fields. Selecting a new variant resolves that preset's defaults; changing an effect type removes incompatible options from the previous type.

The number tokens interaction.track-scale.standard and interaction.track-scale.enhanced define the default expansion ratios. State colors and timing follow the existing theme tokens. Scale remains bounded from 1 to 2. Reduced-motion preferences take precedence over per-instance durations.

Live examples

Try the controls below, then read or copy the complete example to adapt it for your application.

Six palettes, semantic typography, and named theme axes

Control radius · 5px
864 declared color pairs passed; 0 failed; 0 unresolved. Contexts include both schemes, both densities, and the contrast axis.

light palette

This role derives its family from the same theme scope.
accent
secondary
danger
success
warning
info

dark palette

This role derives its family from the same theme scope.
accent
secondary
danger
success
warning
info

Apply builds complete palettes. The report covers declared opaque color pairs, not browser layout, custom backdrops or an accessibility certification.

Read the complete example
'use client';
import { useMemo, useState } from 'react';
import { createBrandTheme, semanticColorRoles, type SemanticColorRole } from '@fayegram-ai/faye-ui/theme/brand';
import { compileThemeCSS, extendTheme } from '@fayegram-ai/faye-ui/theme';
import { auditThemeContrast } from '@fayegram-ai/faye-ui/theme/audit';
import { ThemeScope } from '@fayegram-ai/faye-ui/provider/provider';
import { Button } from '@fayegram-ai/faye-ui/controls/button';
import { TextField } from '@fayegram-ai/faye-ui/controls/text-field';
import { Slider } from '@fayegram-ai/faye-ui/controls/slider';
import { Text, Heading } from '@fayegram-ai/faye-ui/core/text';
import { Grid, Inline, Stack } from '@fayegram-ai/faye-ui/layout/layout';
import { Dialog } from '@fayegram-ai/faye-ui/overlays/dialog';
import { Section, Result } from '../example';
import { Choice } from '../choice';
const initialSeeds: Record<SemanticColorRole, string> = { accent: '#5264aa', secondary: '#7255a3', danger: '#ad353a', success: '#35764f', warning: '#94671c', info: '#326894' };
/** Authoring/compiler code is deliberately confined to this developer-tool example. */
export default function Theming() {
    const [seeds, setSeeds] = useState(initialSeeds);
    const [radius, setRadius] = useState(5);
    const [applied, setApplied] = useState({ seeds: initialSeeds, radius: 5 });
    const [contrast, setContrast] = useState('normal');
    const [font, setFont] = useState('system-ui, sans-serif');
    const theme = useMemo(() => extendTheme(createBrandTheme({ id: 'docs-custom', ...applied.seeds, radius: `${applied.radius}px` }), {
        axes: { contrast: { default: 'normal', values: { normal: {}, high: { 'border.width': { $type: 'length', $value: '3px' } } } } }
    }), [applied]);
    const css = useMemo(() => compileThemeCSS(theme), [theme]);
    const audit = useMemo(() => auditThemeContrast(theme), [theme]);
    return <Section title="Six palettes, semantic typography, and named theme axes" note="Apply builds complete palettes. The report covers declared opaque color pairs, not browser layout, custom backdrops or an accessibility certification.">
        <style>{css}</style>
        <form onSubmit={event => { event.preventDefault(); setApplied({ seeds: { ...seeds }, radius }); }}>
            <Stack gap="3"><div className="docs-example-controls">{semanticColorRoles.map(role => <label className="docs-color-field" key={role}>{role}<input type="color" aria-label={`${role === 'accent' ? 'Accent' : role} seed`} value={seeds[role]} onChange={event => setSeeds(previous => ({ ...previous, [role]: event.target.value }))}/></label>)}</div>
                <div className="docs-example-controls"><div className="docs-choice-field"><span>Control radius · {radius}px</span><Slider.Root value={radius} onValueChange={value => { if (typeof value === 'number') setRadius(value); }} min={0} max={12} step={1}><Slider.Control><Slider.Track><Slider.Indicator/><Slider.Thumb aria-label="Control radius"/></Slider.Track></Slider.Control></Slider.Root></div><Button type="submit">Apply palettes</Button></div>
            </Stack>
        </form>
        <div className="docs-example-controls"><Choice label="Theme contrast axis" value={contrast} onValueChange={setContrast} options={[{value:'normal',label:'Normal'},{value:'high',label:'High'}]}/><Choice label="Theme font family" value={font} onValueChange={setFont} options={[{value:'system-ui, sans-serif',label:'System sans'},{value:'monospace',label:'Monospace'}]}/></div>
        <Result>{audit.passed} declared color pairs passed; {audit.failed} failed; {audit.unresolved} unresolved. Contexts include both schemes, both densities, and the contrast axis.</Result>
        <Grid columns={{ base: 1, md: 2 }} gap="4">{(['light', 'dark'] as const).map(scheme => <ThemeScope key={scheme} themeId={theme.id} scheme={scheme} density="comfortable" axes={{ contrast }} variables={{ '--faye-ui-font-body': font }} data-testid={`custom-${scheme}`} style={{ background: 'var(--faye-ui-color-surface)', color: 'var(--faye-ui-color-text)', padding: 'var(--faye-ui-space-4)' }}>
            <Stack gap="4"><Heading level={4} variant="heading-4">{scheme} palette</Heading><Text data-testid="semantic-body" variant="body">This role derives its family from the same theme scope.</Text><TextField label={`${scheme} input`} placeholder="Type here"/>
                <div className="docs-theme-palettes">{semanticColorRoles.map(role => <div className="docs-theme-palette-row" key={role}><Text variant="label" tone={role}>{role}</Text><Inline wrap><Button tone={role} variant="solid">{role === 'accent' ? 'Primary' : `${role} solid`}</Button><Button tone={role} variant="soft">Soft {role}</Button><Button tone={role}>Outline {role}</Button></Inline></div>)}</div>
                <Dialog.Root><Dialog.Trigger render={<Button />}>Themed overlay</Dialog.Trigger><Dialog.Portal><Dialog.Backdrop /><Dialog.Popup><Dialog.Header><Dialog.Title>Custom theme in a portal</Dialog.Title><Dialog.Description>Scheme, axis selection and declared font variables travel together.</Dialog.Description></Dialog.Header><Dialog.Actions><Dialog.Close render={<Button />}>Done</Dialog.Close></Dialog.Actions></Dialog.Popup></Dialog.Portal></Dialog.Root>
            </Stack>
        </ThemeScope>)}</Grid>
    </Section>;
}