sveltelean
Versionv0.2.0 GitHub

Attributes

AttributeMeaningExample
data-slean="<behavior>"The root of a behavior; a statically discoverable identifierdata-slean="tabs"
data-slean-part="<part>"A structural part inside the rootdata-slean-part="trigger"
data-slean-value="<value>"Identity within a root; on the root, the selected valuedata-slean-value="security"
data-slean-<option>="…"One stable static option per attribute, never JSONdata-slean-activation="manual"
data-slean-disabledThe router skips the root (aria-disabled="true" on the root does the same)data-slean-disabled
data-slean-state="<state>"Exposed only when no platform pseudo-class or ARIA state exists; unused today—
data-slean-theme="<name>"A theme scope: the styles derive their token aliases again on itdata-slean-theme="brand"
data-variant="<variant>"A look, read by stylesheets only (no prefix, like data-size and data-status)data-variant="outline"
data-size="sm|lg"A size, read by stylesheets only; no attribute is mediumdata-size="sm"
data-status="error|warning"The validation status of a form control, read by stylesheets onlydata-status="warning"

Attributes are declarative identifiers, never executable code, so a strict Content Security Policy has nothing to object to. The same markup works in a .svelte file, in plain HTML, in any other renderer, and in server output, and CSS can target every part and every state.

settings.svelte
<div
	data-slean="tabs"
	data-slean-value="account"
	data-slean-activation="manual"
	data-slean-orientation="vertical"
>
	<div role="tablist" aria-label="Settings" aria-orientation="vertical" data-slean-part="list">
		<button type="button" role="tab" data-slean-part="trigger" data-slean-value="account" …>
		<button type="button" role="tab" data-slean-part="trigger" data-slean-value="security" …>
	</div>
	<div role="tabpanel" data-slean-part="panel" data-slean-value="account" …>
	<div role="tabpanel" data-slean-part="panel" data-slean-value="security" hidden …>
</div>

One attribute per option

Configuration is one attribute per stable option. A JSON attribute would cost a parse on every read, complicate escaping, hide the options from CSS selectors and developer tools, and make static validation harder. Options are read at the moment of an interaction with getAttribute, never cached per instance.

HTML
<!-- Not the protocol: one JSON attribute for every option -->
<div data-slean="tabs" data-slean-config='{"activation":"manual","orientation":"vertical"}'>

Inherently dynamic configuration does not belong in attributes. The declarative path is the default; where an application needs to drive a behavior from state, it uses the events below and the pure helpers of the subpath entry (activateTab(), closeMenu()).

Platform state first

State is not mirrored into data-* attributes when the platform or ARIA already expresses it. Stylesheets and behaviors read :checked, :disabled, [open], :popover-open, :focus-visible, aria-selected and aria-expanded. Tabs need a selection state the platform lacks, and aria-selected already is that state, so there is no data-state="active".

CSS
/* Platform state first: no mirrored data-state attributes */
[data-slean='checkbox']:checked { … }
[data-slean='dialog'][open] { … }
[data-slean='popover']:popover-open { … }
[data-slean='disclosure'][open] > summary { … }

/* Tabs have no platform state, so the ARIA state is the selector */
[data-slean='tabs'] [role='tab'][aria-selected='true'] { … }
PrimitiveRootState the stylesheet targets
buttondata-slean="button" on <button> or <a>:hover, :active, :disabled, [aria-disabled], [aria-busy]
dialogdata-slean="dialog" on <dialog>[open], ::backdrop
popoverdata-slean="popover" on [popover]:popover-open
disclosuredata-slean="disclosure" on <details>[open], ::details-content
checkboxdata-slean="checkbox" on <input type="checkbox">:checked, :indeterminate, :disabled, :user-invalid
switchdata-slean="switch" on <input type="checkbox" role="switch">:checked, :disabled
tabsdata-slean="tabs"[aria-selected], [hidden], :disabled
menudata-slean="menu" on [popover][role="menu"]:focus-visible, [aria-checked], [aria-disabled], [aria-expanded]

Events

A behavior reports through native CustomEvents named slean:<event>: bubbling, cancelable, with a small serializable detail. Two exist today. slean:change is dispatched from a tabs root before the DOM is written; preventDefault() keeps the selection. slean:select is dispatched from a menu root after the checked state is updated and before the popover hides; preventDefault() keeps the menu open.

TypeScript
import type { TabsChangeDetail } from '@svelte-lean/primitives/tabs';

const root = document.getElementById('settings-tabs')!;

root.addEventListener('slean:change', (event) => {
	const { value, previous } = (event as CustomEvent<TabsChangeDetail>).detail;
	// Cancelable: the DOM is written only if nobody calls preventDefault().
	if (value === 'billing' && !subscribed) event.preventDefault();
});
TypeScript
import type { MenuSelectDetail } from '@svelte-lean/primitives/menu';

menu.addEventListener('slean:select', (event) => {
	const { value, checked } = (event as CustomEvent<MenuSelectDetail>).detail;
	if (value === 'wrap') editor.wordWrap = checked === true;
});
TypeScript
import { activateTab } from '@svelte-lean/primitives/tabs';

// Selects a value through the same path as a click; returns false when a listener cancelled.
activateTab(root, 'security');

Before a behavior invents an event, the platform's own is preferred: toggle for details and popovers, close and cancel for dialogs, change for form controls. No slean:open exists because toggle already says it.

Static markers

The behavior identifier must be a static string so the build can discover it. A dynamic expression is recorded in the manifest and reported once per file, and needs a manual import of the registration module; the runtime itself does not care how the module got there.

Svelte
<!-- Not discovered: the identifier is an expression -->
<div data-slean={kind}>…</div>

<!-- Discovered: a static identifier -->
<div data-slean="tabs">…</div>

Example

A tabs root with two options set: manual activation and vertical orientation. Arrow keys move focus without selecting; Enter or Space selects; the third trigger is aria-disabled and is skipped. The last event the root dispatched is shown under the frame.

Tabs with options
General preferences.
<div data-slean="tabs" data-slean-value="general" data-slean-activation="manual" data-slean-orientation="vertical">
	<div role="tablist" aria-label="Preferences" aria-orientation="vertical" data-slean-part="list">
		<button
			type="button"
			id="pref-tab-general"
			role="tab"
			aria-controls="pref-panel-general"
			aria-selected="true"
			tabindex="0"
			data-slean-part="trigger"
			data-slean-value="general"
		>
			General
		</button>
		<button
			type="button"
			id="pref-tab-editor"
			role="tab"
			aria-controls="pref-panel-editor"
			aria-selected="false"
			tabindex="-1"
			data-slean-part="trigger"
			data-slean-value="editor"
		>
			Editor
		</button>
		<button
			type="button"
			id="pref-tab-billing"
			role="tab"
			aria-controls="pref-panel-billing"
			aria-selected="false"
			tabindex="-1"
			aria-disabled="true"
			data-slean-part="trigger"
			data-slean-value="billing"
		>
			Billing
		</button>
	</div>
	<div
		id="pref-panel-general"
		role="tabpanel"
		aria-labelledby="pref-tab-general"
		data-slean-part="panel"
		data-slean-value="general"
		tabindex="0"
	>
		General preferences.
	</div>
	<div
		id="pref-panel-editor"
		role="tabpanel"
		aria-labelledby="pref-tab-editor"
		data-slean-part="panel"
		data-slean-value="editor"
		tabindex="0"
		hidden
	>
		Editor preferences.
	</div>
	<div
		id="pref-panel-billing"
		role="tabpanel"
		aria-labelledby="pref-tab-billing"
		data-slean-part="panel"
		data-slean-value="billing"
		tabindex="0"
		hidden
	>
		Billing.
	</div>
</div>
Every option is one attribute on the root. The list carries aria-orientation as well, because assistive technology reads that, not the protocol attribute.

Last event from the root above: none yet

Stability

Renaming a protocol attribute, an event, a CSS token or a package export is a breaking change with a deprecation window: a documented replacement, a development warning, a migration period and a major release to remove. Internal implementation details change freely; the markup an application wrote does not.

Source