Primitives
One package, @svelte-lean/primitives, with 60 primitives. 40 are plain platform markup and ship no behavior JavaScript; 15 share one listener per event type on the document; 5 adds a controller on first use. The same markup works in plain HTML, in a Svelte file and on the server.
On this page
At a glance
Every primitive, live, with its tier and the behavior JavaScript it adds to a page.
- Apple
- Banana
- Cherry
- Ankara
- Lisbon
- Oslo
No city matches
September 2026
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
Analyst, London
- Cart Completed
- Shipping
- Payment
- src
- index.ts
- docs
- guide.md
Shipping
Shipping
Returns
Domain verified
What a primitive is
A primitive is a documented piece of markup with a contract. The contract, a contract.md file next to the primitive's source in packages/primitives/src, fixes the required semantics, the parts, the keyboard
interaction, focus rules, disabled behavior, orientation and RTL, ARIA relationships, what the
server renders, what happens without JavaScript, what the package does not do, and the tests
that prove each point. A typed constant next to it (contract.ts) carries the name,
tier, base, parts, options and events, and is what this site reads.
A primitive is not a component. There is no import per instance and no wrapper: the markup is
the primitive, marked with data-slean="<name>" for the optional stylesheet
and for development diagnostics. A Tier 0 primitive stops there. A Tier 1 primitive adds one
registration per page, @svelte-lean/primitives/<name>/register, which @svelte-lean/vite injects for every static marker it finds; the behavior then
handles events for every root on the page through the shared router in @svelte-lean/core. A Tier 2 primitive registers the same way and, for a root that
is used, keeps a small controller in a WeakMap with listeners scoped to one open
session. The tiers and their budgets are on Runtime tiers; the protocol is on DOM protocol.
The primitives
| Name | Tier | Native base | Behavior JS | Shared listeners | Platform features | Status |
|---|---|---|---|---|---|---|
| Actions | ||||||
| Button | 0 · Native | <button type="button"> | 0 B brotli | none | <button>, :focus-visible, :disabled, aria-busy | stable |
| Button group | 0 · Native | buttons in role="group" | 0 B brotli | none | role="group", logical radii, color-mix(), :focus-visible | stable |
| Toggle | 1 · Delegated | <button type="button" aria-pressed> | 888 B brotli | click | <button>, aria-pressed | stable |
| Toggle group | 1 · Delegated | <button aria-pressed> items in role="group" | 1569 B brotli | click, keydown | <button>, aria-pressed, role="group", :dir(rtl) | stable |
| Toolbar | 1 · Delegated | role="toolbar" | 1494 B brotli | keydown, focusin | role="toolbar", aria-orientation, focusin, :dir(rtl) | stable |
| Forms | ||||||
| Input | 0 · Native | <input type="text|email|password|search|url|tel">, <textarea> | 0 B brotli | none | <input type="text|email|password|search|url|tel">, <textarea>, :user-invalid, :read-only, ::placeholder, field-sizing: content | stable |
| Field | 0 · Native | <label for>, a control, aria-describedby | 0 B brotli | none | <label for>, aria-describedby, aria-invalid, :has() | stable |
| Select | 2 · Lazy | <select> + role=combobox trigger + [popover] + ARIA listbox (role=listbox, option) | 3362 B brotli | click, keydown, pointerdown, pointerover, focusout, change, toggle | <select>, role="combobox", aria-activedescendant, popover="manual", anchor-name, @media (scripting) | stable |
| Checkbox | 0 · Native | <input type="checkbox"> | 0 B brotli | none | <input type="checkbox">, :checked, :indeterminate, :user-invalid | stable |
| Switch | 0 · Native | <input type="checkbox" role="switch"> | 0 B brotli | none | <input type="checkbox">, role="switch", :checked | stable |
| Radio group | 0 · Native | <fieldset> + <input type="radio" name="…"> | 0 B brotli | none | <fieldset>, <legend>, <input type="radio">, name | stable |
| Segmented control | 0 · Native | <fieldset> of <input type="radio"> sharing a name | 0 B brotli | none | <input type="radio">, <fieldset>, <legend>, :has(), :focus-visible | stable |
| Rating | 0 · Native | <fieldset> of <input type="radio"> sharing a name | 0 B brotli | none | <input type="radio">, <fieldset>, <legend>, :has(), inline SVG | stable |
| Slider | 0 · Native | <input type="range"> | 0 B brotli | none | <input type="range">, ::-webkit-slider-thumb, ::-moz-range-progress, accent-color, writing-mode | stable |
| Range slider | 1 · Delegated | two <input type="range"> | 1291 B brotli | input | <input type="range">, <output for>, ::-webkit-slider-thumb, ::-moz-range-thumb, round() | stable |
| Number field | 1 · Delegated | <input type="number">, stepUp()/stepDown() | 1082 B brotli | click | <input type="number">, stepUp(), stepDown(), @media (scripting), :has() | stable |
| One-time code | 0 · Native | <input inputmode="numeric" autocomplete="one-time-code" maxlength> | 0 B brotli | none | <input>, inputmode="numeric", autocomplete="one-time-code", maxlength, pattern, :user-invalid | stable |
| File field | 0 · Native | <input type="file"> | 0 B brotli | none | <input type="file">, multiple, accept, capture, ::file-selector-button | stable |
| File drop | 1 · Delegated | <input type="file"> stretched over a drop area | 1107 B brotli | dragenter, dragleave, drop, change | <input type="file">, drop, dragenter, Intl.ListFormat, @media (scripting) | stable |
| Color field | 0 · Native | <input type="color"> | 0 B brotli | none | <input type="color">, ::-webkit-color-swatch, ::-moz-color-swatch | stable |
| Autocomplete | 0 · Native | <input list> + <datalist> | 0 B brotli | none | <input list>, <datalist>, <option>, :user-invalid | stable |
| Listbox | 1 · Delegated | ARIA listbox markup (role=listbox, option) | 1838 B brotli | click, keydown | role="listbox", role="option", aria-selected, roving tabindex, :dir(rtl) | stable |
| Combobox | 2 · Lazy | <input role="combobox"> + [popover] + ARIA listbox (role=listbox, option) | 2564 B brotli | click, keydown, input, pointerdown, pointerover, focusout, toggle | <input role="combobox">, popover, aria-activedescendant, aria-expanded, KeyboardEvent.isComposing, anchor-name | stable |
| Date and time | ||||||
| Date field | 0 · Native | <input type="date|time|datetime-local|month|week"> | 0 B brotli | none | <input type="date">, time, datetime-local, min, max, step, :user-invalid | stable |
| Calendar | 1 · Delegated | role=grid of day buttons in a <table> | 3925 B brotli | click, keydown, pointerover, pointerout | role="grid", aria-selected, aria-current="date", Intl.DateTimeFormat, :dir(rtl) | stable |
| Date picker | 2 · Lazy | <input type="date"> + role=combobox text input + [popover] + calendar | 4912 B brotli | click, keydown, input, change, pointerdown, focusout, toggle | <input type="date">, role="combobox", popover="manual", anchor-name, @media (scripting), Intl.DateTimeFormat | stable |
| Overlays | ||||||
| Dialog | 0 · Native | <dialog> + command/commandfor | 0 B brotli | none | <dialog>, showModal(), command, commandfor, top layer, ::backdrop | stable |
| Alert dialog | 0 · Native | <dialog role="alertdialog" closedby="none"> + command/commandfor | 0 B brotli | none | <dialog>, role="alertdialog", closedby="none", command, commandfor, autofocus, returnValue | stable |
| Drawer | 0 · Native | <dialog closedby="any"> + command/commandfor | 0 B brotli | none | <dialog>, showModal(), closedby="any", command, commandfor, @starting-style, translate | stable |
| Popover | 0 · Native | [popover] + popovertarget or command/commandfor | 0 B brotli | none | popover, popovertarget, :popover-open, position-area | stable |
| Tooltip | 1 · Delegated | popover="hint" + role="tooltip" + aria-describedby | 1297 B brotli | pointerover, pointerout, focusin, focusout, keydown | popover="hint", role="tooltip", aria-describedby, showPopover({ source }), position-area | stable |
| Hover card | 1 · Delegated | popover="hint" next to its trigger | 1353 B brotli | pointerover, pointerout, focusin, focusout, keydown | popover="hint", showPopover({ source }), position-area, pointerover | stable |
| Menu | 1 · Delegated | [popover] + ARIA menu (role=menu, menuitem) | 1932 B brotli | click, keydown, toggle | popover, role="menu", role="menuitem", aria-checked, ToggleEvent | stable |
| Context menu | 1 · Delegated | contextmenu event + [popover] ARIA menu | 1409 B brotli | contextmenu | contextmenu, showPopover(), role="menu", Shift+F10 | stable |
| Menubar | 1 · Delegated | role="menubar" of popovertarget menu buttons | 1457 B brotli | keydown, focusin, pointerover | role="menubar", popovertarget, showPopover(), :popover-open, :has() | stable |
| Toast | 2 · Lazy | popover="manual" region + aria-live list | 1396 B brotli | click, pointerover, pointerout, focusin, focusout | popover="manual", aria-live="polite", showPopover(), :popover-open | stable |
| Navigation | ||||||
| Tabs | 1 · Delegated | ARIA tabs markup (role=tablist, tab, tabpanel) | 1521 B brotli | click, keydown | role="tablist", role="tab", aria-selected, roving tabindex, hidden | stable |
| Breadcrumb | 0 · Native | <nav> + <ol> + aria-current="page" | 0 B brotli | none | <nav>, <ol>, aria-current="page", :dir(rtl) | stable |
| Pagination | 0 · Native | <nav> + links + aria-current="page" | 0 B brotli | none | <nav>, <a href>, aria-current="page", :dir(rtl) | stable |
| Steps | 0 · Native | <ol> + aria-current="step" | 0 B brotli | none | <ol>, aria-current="step", :has(), CSS counters | stable |
| Tree | 1 · Delegated | role="tree", treeitem, group, aria-expanded | 1919 B brotli | click, keydown | role="tree", treeitem, group, aria-expanded, aria-selected, :dir(rtl) | stable |
| Layout and display | ||||||
| Disclosure | 0 · Native | <details> + <summary> | 0 B brotli | none | <details>, <summary>, name, toggle, ::details-content | stable |
| Accordion | 0 · Native | <details name> group | 0 B brotli | none | <details name>, <summary>, toggle, ::details-content, interpolate-size | stable |
| Card | 0 · Native | <article> | 0 B brotli | none | <article>, ::after, :has() | stable |
| Splitter | 2 · Lazy | role="separator" (WAI-ARIA window splitter) + CSS grid | 1683 B brotli | keydown, pointerdown | role="separator", aria-valuenow, setPointerCapture(), touch-action, CSS grid | stable |
| Scroll area | 0 · Native | overflow: auto + tabindex="0" in role="region" | 0 B brotli | none | overflow: auto, tabindex="0", scrollbar-width, scrollbar-color, scrollbar-gutter, overscroll-behavior, animation-timeline: scroll() | stable |
| Separator | 0 · Native | <hr>, role="separator" | 0 B brotli | none | <hr>, role="separator", aria-orientation | stable |
| Avatar | 0 · Native | <img> with initials behind it | 0 B brotli | none | <img>, alt, role="img", CSS grid | stable |
| Badge | 0 · Native | <span> | 0 B brotli | none | <span>, text | stable |
| Tag | 0 · Native | <span> with an optional remove <button> | 0 B brotli | none | <span>, <button>, aria-label, :has() | stable |
| Keyboard key | 0 · Native | <kbd> | 0 B brotli | none | <kbd>, nested <kbd>, :has() | stable |
| Carousel | 0 · Native | scroll-snap, ::scroll-button(), ::scroll-marker | 0 B brotli | none | scroll-snap, ::scroll-button(), ::scroll-marker, ::scroll-marker-group, scroll-state queries | stable |
| Descriptions | 0 · Native | <dl> with <div> groups of <dt> and <dd> | 0 B brotli | none | <dl>, <dt>, <dd>, CSS grid, min(), max() | stable |
| Timeline | 0 · Native | <ol> of <li> events with <time datetime> | 0 B brotli | none | <ol>, <time datetime>, :has() | stable |
| Feedback | ||||||
| Alert | 0 · Native | <div> + live region roles (status, alert) | 0 B brotli | none | role="status", role="alert", :empty, color-mix() | stable |
| Progress | 0 · Native | <progress value max> | 0 B brotli | none | <progress>, :indeterminate, appearance: none, ::-webkit-progress-value | stable |
| Meter | 0 · Native | <meter min max low high optimum value> | 0 B brotli | none | <meter>, low, high, optimum, ::-webkit-meter-optimum-value | stable |
| Spinner | 0 · Native | role="status" with a text label | 0 B brotli | none | role="status", rotate, color-mix(), prefers-reduced-motion | stable |
| Skeleton | 0 · Native | aria-hidden placeholders in an aria-busy region | 0 B brotli | none | aria-hidden, aria-busy, prefers-reduced-motion, :dir(rtl) | stable |
| Empty state | 0 · Native | a heading, a paragraph and an optional action | 0 B brotli | none | headings, links and buttons, :empty | stable |
Tier, native base and listeners come from BEHAVIORS in @svelte-lean/primitives. Behavior JavaScript is the production size of the
registration module including the shared kernel (packages/primitives/artifacts/size.json); for Tier 0 primitives it is the Svelte Lean chunk of the native-only consumer fixture (fixtures/results.json). Status reads the package version, 0.2.0.
Every primitive can be inspected on the proof page, which mounts one of each and reads the listeners of the document it runs in. The contracts are in packages/primitives/src.
How to read a primitive page
Every primitive page has the same sections in the same order, so a reader learns where to look. The runtime block sits directly under the first paragraph on every page; it reads the tier, listeners and native base from the contract and the bytes from the measurement files, and says "not measured" when a file is missing.
- Example: the contract markup rendered live, with the same source as HTML and as a Svelte file.
- Why this implementation exists: what the browser owns and what Svelte Lean owns.
- Usage: install lines, the import or registration line, the stylesheet import, when to use it.
- Anatomy: the parts table from the contract.
- Runtime profile: how the tier and the bytes in the runtime block were established.
- Accessibility contract: roles, states, relationships, focus and disabled behavior.
- Keyboard: Key · When · Result.
- Platform features: Feature · Baseline · behavior outside the target.
- Without JavaScript, Server rendering, Before hydration: what works when, and what is asserted.
- Styling: the tokens the stylesheet uses, read from the file at build time, and the state selectors.
- Controlled integration: the events and helpers an application uses; what does not exist yet.
- Compatibility notes, Testing, Source: the versioned facts and the repository files.
Compositions and what is not planned
Some components other libraries ship are compositions of these primitives and are documented as
examples rather than added as primitives: a date range picker is two date fields and a calendar
in range mode (Date picker), a time picker is a date field
of type time, a command palette is a dialog holding a combobox (Combobox), a multi-select dropdown is a listbox with data-slean-multiple in a popover (Listbox), a popconfirm and a navigation menu are popovers (Popover), an image preview is a dialog (Dialog). Cascader,
tree select and transfer are applications of tree, listbox and popover; tours, watermarks, QR
codes, charts, rich text and mentions are application features. The catalog and the tier
decision behind every primitive are in docs/catalog.md; the checklist a new primitive must
answer is in CLAUDE.md.
Names the package exports today: button, dialog, popover, disclosure, checkbox, switch, radio-group, tabs, menu, listbox, combobox, date-field, calendar, date-picker, input, field, select, segmented, rating, slider, otp-field, file-field, color-field, autocomplete, alert-dialog, drawer, accordion, card, separator, avatar, badge, tag, kbd, carousel, alert, progress, meter, spinner, skeleton, breadcrumb, pagination, steps, toggle, toggle-group, toolbar, range-slider, number-field, tree, tooltip, toast, button-group, scroll-area, descriptions, timeline, empty, splitter, context-menu, file-drop, hover-card, menubar.