sveltelean Primitives
Versionv0.2.0 GitHub

Example

Plan
Plan
<fieldset data-slean="radio-group">
	<legend>Plan</legend>
	<label><input type="radio" name="plan" value="free" checked /> Free</label>
	<label><input type="radio" name="plan" value="pro" /> Pro</label>
	<label><input type="radio" name="plan" value="team" disabled /> Team (invite only)</label>
</fieldset>
Tier 0: the two sources differ only by the stylesheet import. Tab into the group lands on the checked radio; arrow keys move the selection and skip the disabled one. Everything here works with page JavaScript disabled.

Why this implementation exists

A radio group is the one native control that already has roving focus: the group is a single tab stop on the checked radio, all four arrow keys move the selection and wrap, disabled radios are skipped, and the form submits the value. A <div role="radio"> set re-creates the arrow-key logic in JavaScript and loses the form. Svelte Lean therefore ships a stylesheet that draws the round mark and lays out the fieldset, a contract that names the alternatives, and no arrow-key runtime and no value store.

The browser owns

  • selection: at most one checked radio per name
  • arrow-key movement inside the group, wrapping, and the single tab stop
  • form participation and the change event
  • the group name through <legend>

Svelte Lean owns

  • radio-group.css: fieldset layout, the drawn radio mark, disabled presentation
  • the contract
  • 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
app.css or +layout.svelte
import '@svelte-lean/styles/tokens.css';
import '@svelte-lean/styles/base.css';
import '@svelte-lean/styles/radio-group.css';

Use a radio group for one choice among a few visible options. Render a checked default in most forms; a group with nothing checked is reachable on its first radio. When a fieldset is impossible, a container with role="radiogroup" and aria-labelledby is the alternative. A choice among many options is a native <select>.

without a fieldset
<!-- When a fieldset is impossible (inside a table cell, for example): -->
<div role="radiogroup" aria-labelledby="plan-label">
	<span id="plan-label">Plan</span>
	<label><input type="radio" name="plan" value="free" checked /> Free</label>
	<label><input type="radio" name="plan" value="pro" /> Pro</label>
</div>

Anatomy

PartElementdata-slean-partRequiredNotes
root<fieldset>–yesdata-slean="radio-group" marks it for the styles. A container with role="radiogroup" and aria-labelledby is the alternative.
legend<legend>–yesThe accessible name of the group.
radio<label><input type="radio" name="…" value="…"> …</label>–yesRadios share one name. Render a checked default in most forms.

Runtime profile

The runtime block reads the tier and the events from the contract and the bytes from the native-only consumer fixture: a production Vite build with the Vite plugin whose module graph contains no @svelte-lean/core or @svelte-lean/primitives module. The fixture's markup is button, dialog and popover; the radio path is proven the same way by construction: packages/primitives/src/radio-group has no behavior, validate or register module, the plugin maps radio-group (and the native name radio) to no module, and the playground's bundle spec asserts that the native page, which includes this markup, loads no behavior runtime. Nothing is attached at hydration.

Accessibility contract

  • Radios with the same name inside one <fieldset> with a <legend>, which is the group's accessible name. Each radio has its own label.
  • At most one radio is checked. No aria-checked; the native state is exposed.
  • Focus is native roving: the checked radio, or the first one when none is checked, is the group's tab stop.
  • disabled on an input removes it from arrow navigation; disabled on the fieldset disables the whole group. Arrow-key direction under RTL is defined by the browser; the package changes nothing.

Keyboard

KeyWhenResult
ArrowDown/ArrowRightfocus inside the groupChecks the next radio, wrapping (native)
ArrowUp/ArrowLeftfocus inside the groupChecks the previous radio, wrapping (native)
Spacefocus on an unchecked radioChecks it (native)
Tab/Shift+TabanywhereEnters and leaves the group as one stop, on the checked radio (native)

Platform features

FeatureBaselineOutside the target
<fieldset>, <legend>, <input type="radio">Widely availableNot applicable within the support policy

Without JavaScript

Fully functional. The playground's native page runs the arrow-key and single-tab-stop assertions with page JavaScript disabled.

Server rendering

The checked attribute renders the initial value. No id is required unless for or aria-labelledby is used, and then it is authored.

Before hydration

The radios are interactive as soon as the document is parsed; the native page's assertions run with JavaScript disabled, which covers the interval before any script as well. Hydration attaches nothing to inputs the package does not bind.

Styling

radio-group.css lays out the fieldset as a column, styles the legend and labels, and draws the round mark on the radios with appearance: none and a dot on ::before; disabled radios and the labels that contain them are dimmed, and a disabled fieldset dims the whole group. The tokens it reads and the states it targets:

TokenDefault (light)Applies to
--slean-space-20.5remgap
--slean-space-41remcolumn-gap
--slean-space-10.25remmargin-block-end
--slean-fgvar(--slean-neutral-12)color
--slean-text-sm0.875remfont-size
--slean-font-weight-medium500font-weight
--slean-control-bordervar(--slean-border-strong)border
--slean-radius-full9999pxborder-radius
--slean-control-bgvar(--slean-surface)background
--slean-accent-fgoklch(100% 0 0)color
--slean-duration-fast100mstransition
--slean-easecubic-bezier(0.2, 0, 0, 1)transition
--slean-accentoklch(54% 0.19 258)border-color, background
--slean-dangeroklch(55% 0.2 25)border-color
--slean-control-border-disabledvar(--slean-border)border-color
--slean-control-bg-disabledvar(--slean-muted)background
--slean-fg-mutedvar(--slean-neutral-11)color
--slean-control-border-hovervar(--slean-accent)border-color
--slean-control-height-md2.25reminset

State selectors the stylesheet targets, all from the platform or ARIA: :checked, :disabled, :hover, :user-invalid, [aria-invalid="true"], [type="radio"].

Variant attributes: data-slean-orientation (horizontal).

Controlled integration

The checked radio is the state. Svelte reads and writes it with bind:group; FormData reads it on submit and the native change event reports a change. The package mirrors nothing and dispatches no slean:* event for a radio group; a Svelte adapter is not built and is not needed for this primitive.

plan.svelte
<script lang="ts">
	// The checked radio is the state. bind:group reads and writes it; FormData reads it on submit.
	let plan = $state('free');
</script>

<fieldset data-slean="radio-group">
	<legend>Plan</legend>
	<label><input type="radio" name="plan" value="free" bind:group={plan} /> Free</label>
	<label><input type="radio" name="plan" value="pro" bind:group={plan} /> Pro</label>
</fieldset>

<p>Selected: {plan}</p>

Compatibility notes

Every feature the radio group depends on is Baseline widely available; there is no fallback case. The policy is on Browser support.

Testing

Source