CSS Styles

Base colors

Put theme-root on the app's body and import your stylesheet after Jhana's. The root sets four base colors, then derives the intermediate shades with color-mix(). Override only the colors your app needs.

For raised and recessed surfaces, interaction states and effect strength, see Shading & glow.

Default palette

.theme-root {
	--cAcc: color(display-p3 0 0.43 0.89);
	--cDark: color(display-p3 0.11 0.11 0.13);
	--cLight: color(display-p3 0.6 0.63 0.79);
	--cErr: color(display-p3 1 0 0.33);
}
TokenRole
--cAccSelected controls, primary actions and selected-card text
--cDarkPage, form-container and dialog background
--cLightOrdinary text and icons
--cErrError text and the accent used by theme-error

The error token is --cErr, not --cError. Use text-err for error text. The built-in theme-error class changes the local accent, so a primary button becomes an error-colored action while ordinary text stays light.

Derived shades

DL means dark mixed with light. AD means accent mixed with dark. The number is the percentage of the second color, not opacity. Larger DL values are lighter; larger AD values are darker.

TokenMixture and use
--cDL05Dark with 5% light for input surfaces, passive cards and light-button text
--cDL10 … --cDL7010%, 20%, 30%, 40% and 70% light for neutral shades
--cLighterLight with 50% white for hover, focus and tooltip foregrounds
--cDarkerDark with 30% black for deeper surfaces
--cAD30 / --cAD40Accent with 30% or 40% dark for darker accent shades
--cAD90Accent with 90% dark for primary-button text and selected-card backgrounds

Keep form containers on cDark behind their cDL05 input surfaces. Light buttons use cDL05 text: 95% dark and 5% light, matching Infinipose's plus icons. Primary buttons use an accent background with cAD90 text: 90% dark and 10% accent, matching its import and export icons. Selected cards mix 75% accent with 25% white for their foreground on a subtly tinted dark background, as in the Infinipose rotation-card example. Icon follows the text automatically through its currentColor default. The border retains the original accent.

Local machine accent

Any class beginning with theme- creates a local theme scope. It inherits the root's base colors and recalculates shades for its overrides. These are Infinipose's machine choices and accent colors. Selecting one changes this group's accent without replacing the surrounding palette.

Machine

MachineTheme.svelte

<script lang="ts">
	import { Radio } from '@leon8ix/jhana-ui';

	const uid = $props.id();
	const machines = [
		{ name: 'Duplo', color: 'color(display-p3 0 0.43 0.89)' },
		{ name: 'ColorCut', color: 'color(display-p3 0 0.75 0.48)' },
		{ name: 'Manual', color: 'color(display-p3 0.85 0 0.48)' },
	];
	let machine = $state(machines[1]!);
</script>

<fieldset class="field-group theme-mod" style:--cAcc={machine.color}>
	<legend>Machine</legend>
	<div class="input-group boxed">
		{#each machines as option}
			<Radio name={uid} value={option} label={option.name} bind:group={machine} />
		{/each}
	</div>
</fieldset>

Use the theme class together with the override. Setting only --cAcc on an ordinary descendant leaves inherited shades such as cAD30 computed from the ancestor's accent. A theme scope recalculates them locally. It only sets variables: a custom surface still needs color and background-color when changing its own foreground and background.

Production status colors

Print Meter uses green for online, yellow for offline and red for busy. Its production indicators need bright backgrounds visible across a room. Keep that styling in an app-owned class when upgrading, using the same local theme mechanism. These samples preserve its foreground and background mixtures. The primary card selection style remains available for ordinary choices.

  • Online

  • Offline

  • Busy

DeviceStatus.svelte

<script lang="ts">
	const states = [
		{ name: 'Online', theme: 'online' },
		{ name: 'Offline', theme: 'offline' },
		{ name: 'Busy', theme: 'busy' },
	];
</script>

<ul class="row status-colors">
	{#each states as state}
		<li class="card nohover device-status theme-{state.theme}">
			<h3 class="h2">{state.name}</h3>
		</li>
	{/each}
</ul>

<style>
	/* These colors and status meanings belong to the app. */
	.status-colors {
		--cGreen: color(display-p3 0.51 1 0.41);
		--cYellow: color(display-p3 1 0.95 0.32);
		--cRed: color(display-p3 1 0.32 0.36);
	}
	.theme-online {
		--cAcc: var(--cGreen);
	}
	.theme-offline {
		--cAcc: var(--cYellow);
	}
	.theme-busy {
		--cAcc: var(--cRed);
	}
	.card.device-status {
		color: color-mix(in display-p3, var(--cAcc), var(--cDark) 94%);
		background-color: color-mix(in display-p3, var(--cAcc), var(--cDark) 10%);
		border: var(--borderWidthActive) solid var(--cAD40);
	}
</style>

Sample themes

Try these accents across the documentation. The sample updates only the chosen root variables and restores them when leaving this page. In an app, use a CSS rule for a fixed palette or bind style:--cAcc to state on the theme container, as in the machine example above.

/lib/ui/style.css

/* without styling */

ThemeSwitcher.svelte

<script lang="ts">
	import { CodeBlock, Radio } from '@leon8ix/jhana-ui';

	type Theme = {
		name: string;
		cAcc?: string;
		cDark?: string;
		cLight?: string;
	};
	const themes: Theme[] = [
		{ name: 'Blue' },
		{ name: 'Teal', cAcc: 'color(display-p3 0 0.75 0.48)' },
		{ name: 'Lime', cAcc: 'color(display-p3 .73 1 0)' },
		{ name: 'Yellow', cAcc: 'color(display-p3 1 0.85 0)' },
		{ name: 'Orange', cAcc: 'color(display-p3 1 0.47 0.1)' },
		{ name: 'Red', cAcc: 'color(display-p3 1 0 0.33)' },
		{ name: 'Violet', cAcc: 'color(display-p3 0.42 0 1)' },
	];
	let currTheme: Theme = $state(themes[0]!);
	let currStyle: string = $derived(
		Object.entries(currTheme)
			.filter(([key]) => key !== 'name')
			.map(([key, val]) => `    --${key}: ${val};`)
			.join('\n'),
	);
	let currRule: string = $derived(currStyle ? `.theme-root {\n${currStyle}\n}` : '');
	$effect(() => {
		const rootEl = document.querySelector<HTMLElement>('.theme-root');
		if (!rootEl) return console.warn('Can not theme! Theme root not found.');
		const previous = Object.entries(currTheme)
			.filter(([key]) => key !== 'name')
			.map(([key, value]) => {
				const property = `--${key}`;
				const oldValue = rootEl.style.getPropertyValue(property);
				const priority = rootEl.style.getPropertyPriority(property);
				rootEl.style.setProperty(property, value);
				return { property, oldValue, priority };
			});
		return () => {
			for (const { property, oldValue, priority } of previous) {
				if (oldValue) rootEl.style.setProperty(property, oldValue, priority);
				else rootEl.style.removeProperty(property);
			}
		};
	});
</script>

<div class="stack">
	<fieldset class="input-group" aria-label="Documentation accent">
		{#each themes as theme}
			<Radio name="theme" value={theme} bind:group={currTheme} label={theme.name} />
		{/each}
	</fieldset>

	<CodeBlock name="/lib/ui/style.css" code={currRule || '/* without styling */'} />
</div>