Buttons

Basic Buttons

For most buttons, use the btn class. Its light background has cDL05 text, mixed from 95% cDark and 5% cLight to match Infinipose's plus icons. Add primary for an accent background with cAD90 text, mixed from 90% cDark and 10% cAcc to match its import and export icons.

/routes/buttons/+page.svelte

<div class="btn-group">
	<button type="button" class="btn">Basic</button>
	<button type="button" class="btn primary">Primary</button>
	<button type="button" class="btn dark">Dark</button>
	<button type="button" class="btn" disabled>Disabled</button>
	<button type="button" class="btn box-btn">Boxy</button>
	<button type="button" class="btn primary theme-error">Destructive</button>
</div>

Two Joined Buttons

Wrap two ordinary btn buttons in a btn-split container. Each half runs its own action and can contain text, an icon or both. This uses the same shape, separator and interaction styling as SplitButton, with no component or context-menu root required. Omit either Icon below for a text-only action.

No action selected

JoinedButtonDemo.svelte

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

	let action = $state('No action selected');
</script>

<div class="stack">
	<div class="btn-group">
		{#each ['', 'primary', 'dark'] as variant}
			<div class="btn-split" role="group" aria-label="Save actions">
				<button type="button" class={['btn', variant]} onclick={() => (action = 'Save')}>
					<Icon icon={iconStore} width="1.4em" height="1.4em" />
					<span>Save</span>
				</button>
				<button type="button" class={['btn', variant]} onclick={() => (action = 'Save a copy')}>
					<Icon icon={iconCopy} width="1.4em" height="1.4em" />
					<span>Save a copy</span>
				</button>
			</div>
		{/each}
	</div>
	<p class="hint" role="status">{action}</p>
</div>

Use the same variant on both buttons: btn, btn primary or btn dark. Add box-btn to both for less rounded corners. Both labels can wrap on narrow screens. Disabled and loading states belong to each button independently. Use type="button" unless an action should submit a form. Tab reaches each enabled action, and Enter or Space activates it. With text and an icon, place Icon directly inside the button and wrap the text in a span.

Split Buttons

The main button runs the default action. Click the down arrow or right-click either half to open options anchored to the whole button. Both halves use the same normal, primary or dark styling, with a separator between them. Tab reaches each action separately; Enter or Space activates it.

Mount one CtxMenuRoot in your app layout. SplitButton takes the same menu configuration as other context menus, plus a menuLabel for the arrow's accessible name. Use disabled or loading to block both halves. Popup footer buttons support this through their menu option; see Popups.

No action selected

SplitButtonDemo.svelte

<script lang="ts">
	import { SplitButton } from '@leon8ix/jhana-ui';
	import type { CtxMenuConfig } from '@leon8ix/jhana-ui/ctx-menu';

	let action = $state('No action selected');
	const menu: CtxMenuConfig = {
		items: [{ text: 'Save a copy', action: () => (action = 'Save a copy') }],
	};
</script>

<div class="stack">
	<div class="btn-group">
		{#each ['', 'primary', 'dark'] as variant}
			<SplitButton class={variant} {menu} menuLabel="Save options" onclick={() => (action = 'Save')}>
				Save
			</SplitButton>
		{/each}
	</div>
	<p class="hint" role="status">{action}</p>
</div>

Icon Buttons

Or you can also use it in combination with an icon. Jhana will auto-detect any direct svg or img child. Icon follows the button's text color by default, including primary buttons and hover/focus states. Set color only when the icon needs a different color. In case you need an only-icon no-text button, you should add the icon class.

/routes/icon-button/+page.svelte

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

<div class="input-group">
	<button type="button" class="btn">
		<Icon icon={iconDownload} width="1.4em" height="1.4em" />
		<span>Icon Button</span>
	</button>
	<button type="button" class="btn icon">
		<Icon icon={iconPrint} width="1.4em" height="1.4em" />
	</button>

	<button type="button" class="btn dark icon-root">
		<Icon icon={iconDownload} width="1.4em" height="1.4em" />
		<span>Icon Button</span>
	</button>
	<button type="button" class="btn dark icon-root icon">
		<Icon icon={iconPrint} width="1.4em" height="1.4em" />
	</button>
</div>

Spinner Buttons

To indicate progress, wrap a Spinner in an empty container inside the button. Use a fields grid and put the button group in a field-accessory to center it beside the input itself. Labels and hints occupy separate rows; see Layout and Fields for the shared alignment convention.

ms

SpinnerButtonDemo.svelte

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

	let isProcessing = $state([false, false]);
	let durationMs = $state(1000);

	function onclick(index: number) {
		isProcessing[index] = true;
		setTimeout(() => (isProcessing[index] = false), durationMs);
	}
</script>

<div class="fields">
	<NumberInput label="Duration" unit="ms" bind:value={durationMs} min={0} step={500} />
	<div class="field-accessory">
		<div class="btn-group">
			<button type="button" class="btn" onclick={() => onclick(0)}>
				<span>Spinner Button</span>
				{#if isProcessing[0]}
					<span><Spinner preset="button" color="var(--cLight)" /></span>
				{/if}
			</button>
			<button type="button" class="btn primary" onclick={() => onclick(1)}>
				<span>Primary Spinner Button</span>
				{#if isProcessing[1]}
					<span><Spinner preset="button" color="var(--cAcc)" /></span>
				{/if}
			</button>
		</div>
	</div>
</div>

<style>
	.btn {
		white-space: normal;
	}
</style>

Action Buttons

Set label once: it names the icon button for assistive technology and supplies its Jhana tooltip. Help appears after 800 ms on hover, immediately on keyboard focus, or on a native touch long press. A normal click or tap runs the action. An optional title replaces the tooltip text; it does not create a native browser tooltip. Use an empty title to disable the automatic tooltip. See Help and Tooltips for a working action example. Disabled buttons keep their muted appearance on hover and press.

/routes/action-buttons/+page.svelte

<script lang="ts">
	import { ActionButton, iconActionAdd, iconActionDown, iconActionRemove, iconActionUp } from '@leon8ix/jhana-ui';
</script>

<div class="ctrl-group" style="width: fit-content">
	<ActionButton icon={iconActionAdd} label="Add" />
	<ActionButton icon={iconActionUp} label="Up" disabled />
	<ActionButton icon={iconActionDown} label="Down" />
	<ActionButton icon={iconActionRemove} label="Remove" />
</div>