sveltelean Primitives
Versionv0.2.0 GitHub

Example

Removable tags
  • Design
  • Research
  • Accessibility
  • Archived
<ul aria-label="Topics" class="tags">
	<li>
		<span data-slean="tag">Design
			<button type="button" data-slean-part="remove" aria-label="Remove Design"></button></span>
	</li>
	<li>
		<span data-slean="tag">Research
			<button type="button" data-slean-part="remove" aria-label="Remove Research"></button></span>
	</li>
	<li>
		<span data-slean="tag" data-variant="accent">Accessibility
			<button type="button" data-slean-part="remove" aria-label="Remove Accessibility"></button></span>
	</li>
	<li><span data-slean="tag">Archived</span></li>
</ul>
The tags and buttons are Tier 0 markup. Removing a tag here is page code, the Svelte source's remove(): it drops the tag and moves focus to the next remove button. Without JavaScript the tags render and the buttons do nothing.

Why this implementation exists

A tag is text in a box, and its remove control is a button. The button brings focus, Enter and Space, the click event and a disabled state with it; the tag needs no behavior of its own.

Removal is where component libraries hold state: a list of values, an onRemove callback, a focus move. The list is already the application’s data, so the application removes the item and Svelte removes the element. The contract names the one step that is easy to miss: after a removal the focused button is gone, and focus falls to the page unless the application moves it.

The browser owns

  • the remove button: focus, Enter and Space, the click event, disabled
  • the button’s name from aria-label
  • the list semantics of the <ul> that holds the tags

Svelte Lean owns

  • tag.css: the label box, the accent variant, the cross drawn with two borders, the coarse-pointer target
  • the contract: the button’s name, and where focus goes after a removal
  • documentation

Usage

The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.

npm install @svelte-lean/styles
stylesheets
import '@svelte-lean/styles/tag.css';

Name every remove button after its tag (aria-label="Remove Design"); the cross is drawn and the button has no text. Put a set of tags in a <ul> with a label.

On removal, move focus to the next remove button, the previous one when none follows, or to the input that adds tags when the list is empty. A tag that toggles a filter is a Toggle or a checkbox instead.

Anatomy

PartElementdata-slean-partRequiredNotes
root<span data-slean="tag">–yesThe label as text. data-variant="accent". In a <ul> when there are several.
remove<button type="button" aria-label="Remove …">removenoEmpty; the cross is drawn. The application removes the tag and moves focus.

Runtime profile

Tier 0: the tag has no behavior module, the Vite plugin maps tag to no module, and the native-only fixture proves the path ships no Svelte Lean JavaScript. The click handler in the example is the page's own code.

Accessibility contract

  • The remove part is a native <button> named by aria-label, reachable with Tab and activated with Enter or Space.
  • The cross is drawn on ::before and ::after with borders; forced colors keep it because it is currentColor.
  • After a removal focus goes where the application sends it; without that step it falls to the document body.
  • A disabled remove button is skipped by Tab and drawn at reduced opacity, GrayText in forced colors.

Keyboard

KeyWhenResult
Tab/Shift+TabanywhereMoves between remove buttons; the label is not focusable (native)
Enter/Spacefocus on a remove buttonActivates it; the application removes the tag and moves focus

Platform features

FeatureBaselineOutside the target
<button>Widely availableNot applicable within the support policy
:has()Baseline 2023, widely available since June 2026The padding beside the remove button stays as wide as on the other side

Without JavaScript

The tags render; the remove buttons do nothing, because removal is application code.

Server rendering

Static HTML.

Before hydration

Before hydration the remove buttons do nothing. Hydration attaches the application's click handler; the package attaches nothing.

Styling

tag.css draws the label box, the accent variant, the remove button and its cross, its hover fill, a larger target on coarse pointers, and the disabled state. The tokens it reads:

TokenDefault (light)Applies to
--slean-space-10.25remgap
--slean-space-20.5rempadding-inline
--slean-bordervar(--slean-neutral-6)border
--slean-radius-sm0.375remborder-radius
--slean-mutedvar(--slean-neutral-3)background
--slean-fgvar(--slean-neutral-12)color
--slean-text-sm0.875remfont-size
--slean-accent-softoklch(95% 0.03 258)background
--slean-accent-soft-fgoklch(42% 0.17 258)color
--slean-fg-mutedvar(--slean-neutral-11)color
--slean-duration-fast100mstransition
--slean-easecubic-bezier(0.2, 0, 0, 1)transition
--slean-icon-closeurl("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.5 4.5l7 7m0-7l-7 7' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E")mask
--slean-muted-hovervar(--slean-neutral-4)background

State selectors the stylesheet targets, all from the platform or ARIA: :disabled, :focus-visible, :hover.

Variant attributes: data-variant (accent).

Compatibility notes

Everything here is widely available. :has() (Baseline 2023) only tightens the padding beside the remove button.

Testing

Source