Let the browser do the work.
Native elements first.
Shared listeners, not copies.
Zero bytes where the browser can.
Primitives and a data table for Svelte 5.
A real screen, built from primitives.
2 seats free.
Invitations take a seat once they are accepted.
Actions | ||||
|---|---|---|---|---|
Amara Okafor amara.okafor@harbor.studio | Engineering | Owner | Active | |
Jonas Lindqvist jonas.lindqvist@harbor.studio | Design | Admin | Active | |
Mei Tanaka mei.tanaka@harbor.studio | Operations | Member | Away | |
Rafael Duarte rafael.duarte@harbor.studio | Growth | Member | Active | |
Selin Aydın selin.aydin@harbor.studio | Engineering | Admin | Active | |
Noah Brenner noah.brenner@harbor.studio | Design | Member | Away | |
Priya Raman priya.raman@harbor.studio | Operations | Member | Active | |
Elias Moreau elias.moreau@harbor.studio | Growth | Member | Active | |
Hana Kowalska hana.kowalska@harbor.studio | Engineering | Admin | Away | |
Ömer Say omer.say@harbor.studio | Design | Member | Active |
- hana.kowalska@harbor.studio Design Pending
- lucas.ferreira@harbor.studio Growth Pending
Owner
Admin
Member
Most of it weighs nothing.
40 of 60 primitives are plain HTML with a stylesheet. The browser already ships their behavior, so they add no Svelte Lean JavaScript.
0 B The browser already ships these
- Accordion
- Alert
- Alert dialog
- Autocomplete
- Avatar
- Badge
- Breadcrumb
- Button
- Button group
- Card
- Carousel
- Checkbox
- Color field
- Date field
- Descriptions
- Dialog
- Disclosure
- Drawer
- Empty state
- Field
- File field
- Input
- Keyboard key
- Meter
- One-time code
- Pagination
- Popover
- Progress
- Radio group
- Rating
- Scroll area
- Segmented control
- Separator
- Skeleton
- Slider
- Spinner
- Steps
- Switch
- Tag
- Timeline
Production build, brotli, including the shared kernel each behavior needs. All measurements
Can the browser do it?
One question, asked for every primitive. The answer sets what it costs.
Yes.
Svelte Lean ships nothing.
Native elements and attributes: <dialog>, popover, <details>, form controls. The package adds styles and a contract.
Not alone.
One listener per event, shared.
A single listener per event type on the document serves every instance. State stays in ARIA attributes.
It needs memory.
A controller, created on first use.
Nothing runs until the first interaction. Then a small controller is scoped to that root and cleaned up with it.
Same UI. A fraction of the JavaScript.
Measured against Bits UI, the headless library under shadcn-svelte, with the same Svelte, Vite and build. The markup is about as long or longer; the JavaScript is not.
JavaScript shipped
Source characters, without whitespace
About the same markup. The browser owns the dialog, so there is no JavaScript to ship for it.
<button
type="button"
commandfor="settings"
command="show-modal"
>
Settings
</button>
<dialog
id="settings"
aria-labelledby="settings-title"
aria-describedby="settings-text"
>
<h2 id="settings-title">Settings</h2>
<p id="settings-text">Changes apply to this device.</p>
<button type="button" commandfor="settings" command="close">
Close
</button>
</dialog><script lang="ts">
import { Dialog } from 'bits-ui';
</script>
<Dialog.Root>
<Dialog.Trigger>Settings</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title>Settings</Dialog.Title>
<Dialog.Description>
Changes apply to this device.
</Dialog.Description>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>JavaScript shipped
Source characters, without whitespace
Here the Svelte Lean markup is much longer: roles, ids and relationships are written in the HTML, where Bits UI generates them at runtime.
<script lang="ts">
import '@svelte-lean/primitives/tabs/register';
</script>
<div data-slean="tabs" data-slean-value="account">
<div
role="tablist"
aria-label="Settings"
data-slean-part="list"
>
<button
type="button"
id="tab-account"
role="tab"
aria-controls="panel-account"
aria-selected="true"
tabindex="0"
data-slean-part="trigger"
data-slean-value="account"
>
Account
</button>
<button
type="button"
id="tab-security"
role="tab"
aria-controls="panel-security"
aria-selected="false"
tabindex="-1"
data-slean-part="trigger"
data-slean-value="security"
>
Security
</button>
</div>
<div
id="panel-account"
role="tabpanel"
aria-labelledby="tab-account"
data-slean-part="panel"
data-slean-value="account"
>
Name and email.
</div>
<div
id="panel-security"
role="tabpanel"
aria-labelledby="tab-security"
data-slean-part="panel"
data-slean-value="security"
hidden
>
Password and sessions.
</div>
</div><script lang="ts">
import { Tabs } from 'bits-ui';
</script>
<Tabs.Root value="account">
<Tabs.List>
<Tabs.Trigger value="account">Account</Tabs.Trigger>
<Tabs.Trigger value="security">Security</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="account">Name and email.</Tabs.Content>
<Tabs.Content value="security"
>Password and sessions.</Tabs.Content
>
</Tabs.Root>Bits UI 2.19.3, Svelte 5.57.1, Vite 7.3.6; production builds, brotli, Svelte's own runtime excluded on both sides. Method and cases · Full comparison
A table that stays a table.
Svelte Lean Table renders a real <table>. Search, selection, paging and
column controls are props. Turn them on.
Amara Okafor | Engineering | Europe | $4,501 | 2026-01-07 | |
Jonas Lindqvist | Design | Americas | $12,420 | 2026-06-18 | |
Mei Tanaka | Operations | Asia | $20,339 | 2026-02-01 | |
Rafael Duarte | Growth | Europe | $28,258 | 2026-07-12 | |
Selin Aydın | Engineering | Americas | $36,177 | 2026-03-23 | |
Noah Brenner | Design | Asia | $44,096 | 2026-08-06 |
<DataTable
{data}
{columns}
getRowId={(row) => row.id}
searchable
selection="multiple"
state={{ pageSize: 6 }}
/>A thousand tabs. No new listeners.
Behavior is registered once per page, not copied into every instance. Mount more tabs and read the document again.
- Tabs roots on this page
- 100
- On tabs elements
- …
- Listeners on the document
Measured against Bits UI
A thousand tabs mounted into an empty page, median of nine runs.
Time to mount 6.3× faster
JavaScript heap 21.5× smaller
Headless Chrome, client-side mount, heap after garbage collection. Open the proof page
The bracket is the browser. The square is Svelte Lean.
Start with the HTML you already know.
npm install @svelte-lean/primitivespnpm add @svelte-lean/primitivesyarn add @svelte-lean/primitivesbun add @svelte-lean/primitives