@swiss-ui/svelte
v0.1.0
Published
Svelte components for Swiss UI design system
Readme
@swiss-ui/svelte
Svelte 5 components for the Swiss UI design system.
Installation
npm install @swiss-ui/svelte svelteArchitecture
Every component ships in two modes:
- Headless (
@swiss-ui/svelte) — logic, state, ARIA, no classes by default - Styled (
@swiss-ui/svelte/styled) — wraps headless withswiss-*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/getContextwithSymbolkeys 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>SwissTooltip
| 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-describedbyon form fieldsaria-modal,aria-labelledby,aria-describedbyon dialogsaria-checked="mixed"on indeterminate checkboxesrole="switch"witharia-checkedon SwissSwitchrole="radiogroup"witharia-orientationon 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 accessdocumentrun only client-side- Portal renders nothing during SSR
document.body.stylemodifications are guarded by$effect()
Build
npm run build # svelte-package → dist/
npm run dev # watch mode
npm run check # svelte-check
npm test # vitest runIndividual Component Imports
import { SwissButton } from '@swiss-ui/svelte/SwissButton'
import { SwissModal } from '@swiss-ui/svelte/SwissModal'
import { SwissDropdown } from '@swiss-ui/svelte/SwissDropdown'