CSS Styles

Shading and glow

Shading gives a surface depth with white and black inset shadows. Glow adds a soft light outside its edges. Jhana's controls already configure these effects for their interaction states. Use their defaults first, and configure a custom surface when it needs the same treatment. The complete consumer and agent reference ships with the package in docs/shading.md.

Built-in control states

Hover, press and Tab through these controls. Buttons rise on hover or keyboard focus and become recessed when pressed. Inputs become recessed while editing. The boxed toggle and radio group keep their outer surfaces flat while their indicators respond. The progress track stays recessed and its value stays raised and glowing. “Without glow” changes just the glow brightness; its shading and focus outline remain intact.

Sides

ControlStates.svelte

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

	const uid = $props.id();
	let copies = $state<number | null>(12);
	let collate = $state(true);
	let sides = $state('Both sides');
</script>

<div class="stack controls">
	<div class="btn-group">
		<button type="button" class="btn">Light</button>
		<button type="button" class="btn primary">Primary</button>
		<button type="button" class="btn dark">Dark</button>
		<button type="button" class="btn primary" style:--glow-brightness="0%">Without glow</button>
		<button type="button" class="btn" disabled>Disabled</button>
		<button type="button" class="btn primary" disabled aria-label="Loading">
			<Spinner preset="button" color="currentColor" />
		</button>
	</div>
	<div class="fields">
		<NumberInput label="Copies" bind:value={copies} min={1} />
		<div class="field-accessory"><Toggle boxed label="Collate" bind:checked={collate} /></div>
	</div>
	<fieldset class="field-group">
		<legend>Sides</legend>
		<div class="input-group boxed">
			<Radio name={uid} value="Both sides" label="Both sides" bind:group={sides} />
			<Radio name={uid} value="Front only" label="Front only" bind:group={sides} />
		</div>
	</fieldset>
	<ProgressBar value={0.6} width="min(100%, 24em)" aria-label="Example progress" />
</div>

<style>
	.controls {
		padding: var(--space-3);
		gap: var(--space-4);
		background: var(--cDark);
	}
</style>
ControlDepth and glow
Light / primary / dark buttonFlat at rest. Raised on hover/focus, recessed on press. Primary glow mixes the light and accent colors
Split button / popup footerUses the same button states. Each half keeps its own focus outline
Loading buttonA contained progressbar triggers muted colors, recessed shading and reduced glow. Use disabled or an app guard for keyboard activation too
Disabled buttonBlocks pointer interaction and selects the recessed direction. It does not automatically acquire the loading palette or a visible shadow
Input / NumberInput / ComboBoxShallow recess and neutral glow on hover, deeper recess and darker background on keyboard focus. Disabled/read-only inputs skip these effects
ColorPicker triggerRetains the focused input surface while its editing dialog is open
Radio / ToggleTrack recedes and knob rises on hover/focus. Checked knobs glow with the accent. The small unchecked radio dot has no glow or shading
Action-list itemRaised on hover/focus or .focus, recessed on press. No outer glow
Table rowRaised on hover. Press reduces the offset to 0.6 without inverting it. No outer glow or added keyboard behavior
ProgressBarPersistent recessed track and raised value with accent glow. Shading scales with its height prop
ColorPicker channel trackPersistent raised shading over the gradient. Keyboard focus adds neutral glow

Custom surfaces

The shade and glow classes enable the same three-layer shadow stack. Their names express intent; either can do both effects. All strengths start at zero, so either class alone is invisible. Supply your own background, radius and spacing. A positive direction makes the upper edge light and the lower edge dark; a negative direction reverses them. Adding primary to a bare glow surface only changes its glow tint, not its background.

Raised
Recessed
Neutral glow
Accent glow

Surfaces.svelte

<script lang="ts"></script>

<div class="row surfaces">
<div class="shade surface raised">Raised</div>
<div class="shade surface recessed">Recessed</div>
<div class="glow surface luminous">Neutral glow</div>
<div class="glow surface luminous primary">Accent glow</div>
</div>

<style>
.surfaces {
	gap: var(--space-4);
	padding: var(--space-3);
}
.surface {
	padding: 1em 1.5em;
	border-radius: var(--rad);
	background: var(--cDL20);
}
.surface.raised,
.surface.recessed {
	--shade-size: 0.6em;
	--shade-offset: 0.4em;
	--shade-darkness: 35%;
	--shade-brightness: 14%;
}
.surface.recessed {
	--shade-invert: -1;
}
.surface.luminous {
	--glow-size: 1.3em;
	--glow-brightness: 35%;
}
</style>

Configuration

The shadow stack contains one centered outer glow followed by white and black inset shadows with opposite vertical offsets. Each has zero horizontal offset and spread. Settings apply to the element painting those shadows, and the effect follows its border radius without taking up layout space.

PropertyDefaultMeaning
--shade-size0emNonnegative length. Blur radius of both inset shadows
--shade-offset0emLength. Use a positive vertical offset and choose its direction separately
--shade-invert1Unitless multiplier: 1 raised, -1 recessed. Fractions reduce the offset. Zero centers the shadows rather than disabling them
--shade-darkness0%Black inset-shadow opacity, 0% to 100%
--shade-brightness0%White inset-shadow opacity, 0% to 100%
--glow-size0emNonnegative length. Blur radius of the outer glow
--glow-brightness0%Glow color coverage mixed with transparent, 0% to 100%. This is opacity, not color lightness
--glow-is-accent00 uses cLight, 1 uses an equal mix of cLight and cAcc. Intermediate values blend toward that mixture

Use em for effects that should scale with the surface's font size. Existing buttons and menu items use 14px inset blur and 11px offset, with glow sizes in em. White and black shading is independent of the palette. Neutral glow follows cLight, not currentColor; primary glow is a light/accent mixture, not pure cAcc.

Try the settings

This surface holds its settings continuously so you can compare them. Set direction to -1 to recess it or 0.6 to reduce the raised offset. Set both shading opacities to zero for glow alone, or glow brightness to zero for shading alone. The background stays the same when changing the accent mix. The size controls here use em and limit the preview to 3em; this is a playground limit, not a library limit. Empty fields preview as zero.

em
em
%
%
em
%
Configured surface

Current settings (apply to a .shade element)

--shade-size: 0.6em;
	--shade-offset: 0.4em;
	--shade-invert: 1;
	--shade-darkness: 35%;
	--shade-brightness: 14%;
	--glow-size: 1.3em;
	--glow-brightness: 30%;
	--glow-is-accent: 1;

ShadingPlayground.svelte

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

	let size = $state<number | null>(0.6);
	let offset = $state<number | null>(0.4);
	let invert = $state<number | null>(1);
	let darkness = $state<number | null>(35);
	let brightness = $state<number | null>(14);
	let glowSize = $state<number | null>(1.3);
	let glowBrightness = $state<number | null>(30);
	let accent = $state<number | null>(1);
	const settings = $derived(`--shade-size: ${size ?? 0}em;
	--shade-offset: ${offset ?? 0}em;
	--shade-invert: ${invert ?? 0};
	--shade-darkness: ${darkness ?? 0}%;
	--shade-brightness: ${brightness ?? 0}%;
	--glow-size: ${glowSize ?? 0}em;
	--glow-brightness: ${glowBrightness ?? 0}%;
	--glow-is-accent: ${accent ?? 0};`);
</script>

<div class="stack playground">
	<div class="fields">
		<NumberInput label="Shade blur" unit="em" bind:value={size} min={0} max={3} step={0.1} />
		<NumberInput label="Shade offset" unit="em" bind:value={offset} min={0} max={3} step={0.1} />
		<NumberInput label="Direction" bind:value={invert} min={-1} max={1} step={0.1} />
		<NumberInput label="Darkness" unit="%" bind:value={darkness} min={0} max={100} />
		<NumberInput label="Brightness" unit="%" bind:value={brightness} min={0} max={100} />
		<NumberInput label="Glow blur" unit="em" bind:value={glowSize} min={0} max={3} step={0.1} />
		<NumberInput label="Glow brightness" unit="%" bind:value={glowBrightness} min={0} max={100} />
		<NumberInput label="Accent mix" bind:value={accent} min={0} max={1} step={0.1} />
	</div>
	<div class="preview">
		<div class="shade sample" style={settings}>Configured surface</div>
	</div>
	<CodeBlock name="Current settings (apply to a .shade element)" code={settings} />
</div>

<style>
	.playground {
		background: var(--cDark);
	}
	.preview {
		padding: 3em;
		display: grid;
		place-items: center;
	}
	.sample {
		padding: 1em 1.5em;
		border-radius: var(--rad);
		background: var(--cDL20);
	}
</style>

Local configuration and inheritance

Each participating element resets all eight properties. Setting them on theme-root or a component wrapper does not configure the nested controls. Target the actual btn, input, track, knob or custom shade/glow element. Inline settings on a native control override its normal state rules, as in the “Without glow” button above. For shared app settings, define an app-owned variable on the ancestor and reference it on the affected controls.

App setting

<div style:--app-glow-strength="15%">
	<button type="button" class="btn primary" style:--glow-brightness="var(--app-glow-strength)">
		Save
	</button>
</div>

Stylesheet overrides belong after Jhana's CSS and must meet the specificity of the built-in state rules. NumberInput and Toggle forward class/style to wrappers; ComboBox provides inputClass/inputStyle for its input. SplitButton forwards style to its group and class to both buttons. ProgressBar forwards them to its track, not its value. A scoped Svelte stylesheet needs a suitable :global(...) descendant selector to reach component internals. Check the computed styles in every affected state.

To turn off shading, set both shade opacities to 0%. To turn off normal glow, set glow brightness to 0%. A new box-shadow declaration replaces all three layers; box-shadow: none also removes all three. Use a separate wrapper for an independent elevation shadow when you need both. There is no global master strength or off switch.

Theme colors and independent glow

Prefer a local theme scope when the surface and its glow share a palette. The glow helpers are computed on each participating element. The base helper is a color plus a percentage; the final helper is a complete CSS color used by box-shadow.

Built-in glow calculation

--glow-base-color: color-mix(in display-p3, var(--cLight), var(--cAcc) calc(var(--glow-is-accent) * 50%))
	var(--glow-brightness);
--glow-color: color-mix(in display-p3, transparent, var(--glow-base-color));

For an intentionally independent glow color, replace the base helper with a color and the brightness percentage. Overriding glow-color directly bypasses both brightness and accent mixing.

Independent error-colored glow

.glow.status-light {
	--glow-size: 1.3em;
	--glow-brightness: 25%;
	--glow-base-color: var(--cErr) var(--glow-brightness);
}

Transitions, clipping and focus

Shade and glow do not add transitions. Built-in controls own theirs; for a custom changing surface, use transition: box-shadow var(--trans). Animate the computed shadows rather than the unregistered custom properties. The root defines --trans as --timeSm (0.2s) and --tFunc (cubic-bezier(0.27, 0.17, 0.2, 1)). Those tokens inherit and affect other transitions too. There is no shared reduced-motion rule for CSS shading; custom effects can disable their transition with a local prefers-reduced-motion media query.

Outer glow reserves no space. Allow room with container gaps and padding, especially inside scrolling regions. Overflow clipping and paint containment on ancestors can cut it off. Keep a menu's scrolling and give its controls room inside it. Clipping or overlapping split-button halves can also hide their shading and outlines.

Borders and focus outlines are separate from this system. Keep keyboard focus visible when changing effects, and use native disabled and checked states. Shading does not make a custom surface interactive. Glow alone is not a label, a persistent selection indicator or a substitute for accessible focus feedback.

Separate effects

These effects have their own styling and do not respond to the shared shade/glow settings.

SurfaceConfiguration
CardsNeutral elevation or primary accent selection glow. Theme colors and card rules control it; nohover suppresses hover/focus changes
Dialogs and tooltipsDark elevation shadows on their own surfaces
ButtonClosecErr controls its hover glow and red active/focused treatment
PageProgress / PageProgressRootGradients, blur and drop shadows. Public color and strength props control rendering; its animation handles glow fading and reduced motion
ColorPicker thumbAn inset spread draws the filled dot/hollow ring. The channel track uses shared shading
CodeBlockA separate dark shadow on its container

ActionButton uses icon colors and opacity, not shared shadows. See Visuals for progress rendering and ColorPicker for track and thumb behavior.

Older app overrides

The old glow-offset and glow-accent properties are obsolete. Use --glow-brightness and --glow-is-accent. An offset length cannot become an opacity percentage by renaming it: compare the appearance and choose a brightness. The current glow fades through transparent color, not by changing shadow spread or offset.