@swiss-ui/svelte
Svelte 5 components for the Swiss UI design system.
Installation
npm install @swiss-ui/svelte svelte
Architecture
Every component ships in two modes:
- Headless (
@swiss-ui/svelte) — logic, state, ARIA, no classes by default
- Styled (
@swiss-ui/svelte/styled) — wraps headless with swiss-* classes from @swiss-ui/core
<!-- Headless: bring your own styles -->
<script>
import { SwissButton } from '@swiss-ui/svelte'
</script>
<SwissButton variant="solid">Save</SwissButton>
<!-- Styled: uses Swiss UI design tokens -->
<script>
import { StyledSwissButton } from '@swiss-ui/svelte/styled'
</script>
<StyledSwissButton variant="solid">Save</StyledSwissButton>
Svelte 5 Patterns
All components use Svelte 5 runes exclusively:
$state() for reactive state
$derived() for computed values
$effect() for side effects
$props() with typed interfaces
$bindable() for two-way binding
- Snippets (
{#snippet} / {@render}) instead of named slots
setContext / getContext with Symbol keys for compound components
Components
Primitives
| Component |
Props |
Description |
SwissBox |
el, class |
Polymorphic box element |
SwissTextPrimitive |
el, class |
Polymorphic text element |
Layout
| Component |
Props |
Description |
SwissContainer |
size (sm|md|lg|xl|full) |
Constrained width container |
SwissStack |
direction, gap, align, justify |
Flex stack |
SwissGrid |
cols, gap, align |
CSS grid wrapper |
SwissGridItem |
colSpan, rowSpan |
Grid cell |
Typography
| Component |
Props |
Description |
SwissHeading |
el, level (1–6), size, weight, align |
Heading h1–h6 |
SwissText |
el, size, weight, align, color, truncate |
Text paragraph or span |
Controls
| Component |
Props |
Snippets |
Bindable |
SwissButton |
variant, size, loading, disabled, el, type |
leftIcon, rightIcon |
— |
SwissInput |
size, type, disabled, invalid, id, describedBy |
leftAddon, rightAddon |
bind:value |
SwissTextarea |
rows, resize, autoResize, disabled, invalid |
— |
bind:value |
SwissSelect |
size, disabled, invalid, id |
children (options) |
bind:value |
SwissCheckbox |
indeterminate, disabled, invalid, id, name, value |
children (label) |
bind:checked |
SwissRadio |
value, disabled, invalid, id |
children (label) |
— |
SwissRadioGroup |
name, disabled, orientation |
children |
bind:value |
SwissSwitch |
disabled, id, name |
children (label) |
bind:checked |
Feedback
| Component |
Props |
Snippets |
SwissBadge |
variant (solid|outline|subtle), color |
children |
SwissAlert |
variant (info|success|warning|error) |
icon, title, description, actions |
SwissSpinner |
size, color, label |
— |
Overlay
SwissModal
Compound component. Manages focus trap, Escape key, scroll lock, and ARIA.
| Part |
Props |
Bindable |
SwissModal |
onclose |
bind:open |
SwissModalOverlay |
closeOnClick |
— |
SwissModalContent |
— |
— |
SwissModalHeader |
— |
— |
SwissModalBody |
— |
— |
SwissModalFooter |
— |
— |
SwissModalCloseButton |
— |
— |
<script>
import {
SwissModal,
SwissModalOverlay,
SwissModalContent,
SwissModalHeader,
SwissModalBody,
SwissModalFooter,
SwissModalCloseButton,
} from '@swiss-ui/svelte'
let open = $state(false)
</script>
<button onclick={() => open = true}>Open</button>
<SwissModal bind:open>
<SwissModalOverlay />
<SwissModalContent>
<SwissModalHeader>
Title
<SwissModalCloseButton />
</SwissModalHeader>
<SwissModalBody>Content goes here</SwissModalBody>
<SwissModalFooter>
<SwissButton onclick={() => open = false}>Close</SwissButton>
</SwissModalFooter>
</SwissModalContent>
</SwissModal>
| Props |
Snippets |
Bindable |
placement, delay, disabled |
children (trigger), content |
bind:open |
<script>
import { SwissTooltip } from '@swiss-ui/svelte'
</script>
<SwissTooltip placement="top">
<button>Hover me</button>
{#snippet content()}
Tooltip text
{/snippet}
</SwissTooltip>
SwissDropdown
Compound component with full keyboard navigation (Arrow, Enter, Escape, Home, End).
| Part |
Props |
Bindable |
SwissDropdown |
— |
bind:open |
SwissDropdownTrigger |
— |
— |
SwissDropdownContent |
placement |
— |
SwissDropdownItem |
disabled, onselect |
— |
SwissDropdownSeparator |
— |
— |
SwissDropdownLabel |
— |
— |
<script>
import {
SwissDropdown,
SwissDropdownTrigger,
SwissDropdownContent,
SwissDropdownItem,
SwissDropdownSeparator,
SwissDropdownLabel,
} from '@swiss-ui/svelte'
</script>
<SwissDropdown>
<SwissDropdownTrigger>Menu</SwissDropdownTrigger>
<SwissDropdownContent>
<SwissDropdownLabel>Actions</SwissDropdownLabel>
<SwissDropdownItem onselect={() => console.log('edit')}>Edit</SwissDropdownItem>
<SwissDropdownItem onselect={() => console.log('copy')}>Copy</SwissDropdownItem>
<SwissDropdownSeparator />
<SwissDropdownItem disabled>Delete</SwissDropdownItem>
</SwissDropdownContent>
</SwissDropdown>
Actions
Import from @swiss-ui/svelte/actions:
| Action |
Parameters |
Description |
use:portal |
target?: string | HTMLElement |
Renders element into a different DOM node |
use:focusTrap |
active: boolean |
Traps keyboard focus within element |
use:clickOutside |
handler: () => void |
Fires handler on click outside element |
use:escapeKey |
handler: () => void |
Fires handler on Escape keydown |
use:autoPlacement |
{ trigger, placement, offset } |
Positions element relative to trigger |
<script>
import { portal, focusTrap, clickOutside, escapeKey } from '@swiss-ui/svelte/actions'
let open = $state(false)
</script>
{#if open}
<div
use:portal={'body'}
use:focusTrap={open}
use:clickOutside={() => open = false}
use:escapeKey={() => open = false}
>
Popup content
</div>
{/if}
RadioGroup Example
<script>
import { SwissRadioGroup, SwissRadio } from '@swiss-ui/svelte'
let selected = $state('a')
</script>
<SwissRadioGroup bind:value={selected} orientation="horizontal">
<SwissRadio value="a">Option A</SwissRadio>
<SwissRadio value="b">Option B</SwissRadio>
<SwissRadio value="c" disabled>Option C</SwissRadio>
</SwissRadioGroup>
Headless vs Styled
<!-- Headless: data attributes for styling -->
<SwissButton variant="solid" size="md" data-variant="solid">
Click
</SwissButton>
<!-- Styled: adds swiss-button class -->
<StyledSwissButton variant="solid">
Click
</StyledSwissButton>
Accessibility
All components follow WCAG 2.1 AA:
- Semantic HTML elements and ARIA roles
aria-invalid, aria-describedby on form fields
aria-modal, aria-labelledby, aria-describedby on dialogs
aria-checked="mixed" on indeterminate checkboxes
role="switch" with aria-checked on SwissSwitch
role="radiogroup" with aria-orientation on SwissRadioGroup
- Full keyboard navigation on Dropdown (Arrow, Enter, Escape, Home, End)
- Focus trap on Modal with first-element focus on open
- Focus returns to trigger on Modal/Dropdown close
SvelteKit SSR
All components are SSR-safe:
- Svelte actions run only in the browser (mount-time)
$effect() blocks that access document run only client-side
- Portal renders nothing during SSR
document.body.style modifications are guarded by $effect()
Build
npm run build # svelte-package → dist/
npm run dev # watch mode
npm run check # svelte-check
npm test # vitest run
Individual Component Imports
import { SwissButton } from '@swiss-ui/svelte/SwissButton'
import { SwissModal } from '@swiss-ui/svelte/SwissModal'
import { SwissDropdown } from '@swiss-ui/svelte/SwissDropdown'