A minimal, highly customizable Svelte component library — zero CSS framework required, zero configuration needed to look good.
Most Svelte UI libraries make you choose between two bad deals:
| Library | The problem |
|---|---|
| Tailwind-first (ShadCN, Skeleton) | Pollutes your markup with utility classes, requires Tailwind as a hard runtime dependency, and breaks if you're not on a Tailwind project |
| Framework-bundled (Flowbite, daisyUI) | Ships megabytes of CSS you don't control, is painful to theme beyond their provided presets, and forces you into their visual language |
svelte-weave takes a different approach:
tailwind.config.js changes, no CSS preprocessor, no purge setup. It works in plain Svelte, SvelteKit, or any build tool.--sw-primary and the entire primary color scale (50–950 shades, hover, active, foreground) updates automatically via color-mix() — no Sass, no build step.window or document access without guards. Deploys to Vercel, Netlify, and Cloudflare Pages out of the box.--sw-* vars so components match your design system automatically.npm install svelte-weave
Peer dependency: Svelte 4 or later.
// In your layout, root +layout.svelte, or global CSS entry point:
import 'svelte-weave/styles';
Or link it in
src/app.html:<link rel="stylesheet" href="/node_modules/svelte-weave/dist/styles/index.css" />
<script>
import { Button, Input, Card, Alert } from 'svelte-weave';
</script>
<Card padded>
<Input label="Email" placeholder="[email protected]" />
<Button variant="primary" fullWidth>Sign in</Button>
</Card>
<Alert variant="success" title="All good!">Your changes were saved.</Alert>
Paste this into src/app.html immediately before %sveltekit.head%:
<script>(function(){try{var t=localStorage.getItem('sw-theme');if(t==='dark'){document.documentElement.classList.add('dark');}else if(t==='light'){document.documentElement.classList.add('light');}else if(window.matchMedia('(prefers-color-scheme: dark)').matches){document.documentElement.classList.add('dark');}}catch(e){}})();</script>
This prevents a flash of the wrong theme on first load.
| Component | Description |
|---|---|
Button |
Full-featured button with variants, sizes, loading state, icon slots, click effects, and optional link rendering via href |
Badge |
Inline label pill for status, counts, and tags |
Avatar |
User avatar from image, initials, or fallback |
Spinner |
Accessible loading indicator |
Progress |
Progress bar with optional label, striped/animated variants |
Divider |
Horizontal or vertical rule with optional centered label |
Heading |
Semantic h1–h6 with independent visual sizing |
Text |
Versatile text block: body, lead, caption, overline, inline code |
Link |
Accessible anchor with underline control and external link support |
| Component | Description |
|---|---|
Input |
Text input with label, helper text, error state, icon slots |
Textarea |
Multi-line input with optional auto-resize |
Checkbox |
Single checkbox with indeterminate support |
RadioGroup + Radio |
Accessible radio group; horizontal or vertical layout |
Switch |
Toggle switch with label |
Select |
Native-style select with option objects, placeholder, and error state |
| Component | Description |
|---|---|
Alert |
Inline alert banner: info / success / warning / danger variants, optional title, dismissible |
toast + ToastContainer |
Programmatic toast notifications — call toast.success() from anywhere; render <ToastContainer /> once in your layout |
Tooltip |
Hover tooltip with configurable placement and delay |
| Component | Description |
|---|---|
Tabs + Tab + TabPanel |
Tabbed content area — underline or pills variant, full-width option |
Breadcrumbs |
Breadcrumb trail from an items array, custom separator support |
Pagination |
Page selector with sibling windows and optional edge buttons |
Menu + MenuItem |
Dropdown menu with 4 placement options and a danger item variant |
| Component | Description |
|---|---|
Card |
Surface container with optional padding, border, shadow, hover effect, and link rendering |
Table |
Data table with column definitions, optional sorting, striped/hoverable/compact/sticky header variants |
Accordion + AccordionItem |
Collapsible Q&A / content sections; supports multi-open mode |
| Component | Description |
|---|---|
Modal |
Accessible dialog with title, description, closable overlay, and 5 sizes |
Drawer |
Side panel from any edge (left / right / top / bottom) |
Popover |
Floating content attached to a trigger, 6 placement options |
| Export | Description |
|---|---|
ThemeToggle |
Drop-in button to toggle dark / light / system mode |
theme |
Svelte store — set(), toggle(), setDark(), setLight(), setSystem() |
resolvedTheme |
Derived store that resolves 'system' to the actual 'light' or 'dark' value |
themeScript |
Minified FOUC-prevention inline script string (embed in app.html) |
All component styles use --sw-* CSS custom properties. Override them in your own CSS to retheme the entire library.
:root {
--sw-primary: #7c3aed; /* violet — the whole 50–950 scale updates automatically */
--sw-radius: 0.25rem; /* sharper corners everywhere */
}
| Category | Variables |
|---|---|
| Primary | --sw-primary, --sw-primary-50 … --sw-primary-950, --sw-primary-hover, --sw-primary-active, --sw-primary-foreground |
| Danger | --sw-danger + same scale pattern |
| Success | --sw-success + same scale pattern |
| Warning | --sw-warning + same scale pattern |
| Secondary | --sw-secondary, --sw-secondary-hover, --sw-secondary-foreground |
| Neutral | --sw-text, --sw-text-muted, --sw-bg, --sw-border, --sw-ring |
| Shape | --sw-radius, --sw-radius-sm, --sw-radius-lg, --sw-radius-full |
| Spacing | --sw-spacing-xs → --sw-spacing-xl |
| Typography | --sw-font-size-sm/md/lg, --sw-font-weight-normal/medium/semibold |
| Motion | --sw-transition (150ms), --sw-transition-slow (300ms) |
| Shadows | --sw-shadow-sm, --sw-shadow, --sw-shadow-lg |
svelte-weave ships a complete three-layer dark mode system:
| Layer | How it works | JS required? |
|---|---|---|
| OS preference | @media (prefers-color-scheme: dark) on :root:not(.light) |
No |
| Manual dark | Add .dark class to <html> |
Yes |
| Manual light | Add .light class to <html> (prevents OS override) |
Yes |
<script>
import { ThemeToggle, theme, resolvedTheme } from 'svelte-weave';
</script>
<!-- Drop-in toggle button -->
<ThemeToggle />
<!-- With system option (three-way: light → dark → system) -->
<ThemeToggle showSystem />
<!-- With a text label in the button -->
<ThemeToggle showSystem>Theme</ThemeToggle>
<!-- Programmatic control -->
<button on:click={theme.setDark}>Force dark</button>
<button on:click={theme.setSystem}>Follow OS</button>
<!-- Read the current value -->
<p>Current theme: {$resolvedTheme}</p>
import { theme, resolvedTheme } from 'svelte-weave';
theme.set('dark'); // set explicitly
theme.set('light');
theme.set('system'); // follow OS; removes localStorage entry
theme.toggle(); // flip light ↔ dark
theme.setDark();
theme.setLight();
theme.setSystem();
$theme // 'light' | 'dark' | 'system'
$resolvedTheme // 'light' | 'dark' — never 'system'
use:reveal)Animates an element in when it enters the viewport using IntersectionObserver. SSR-safe and honours prefers-reduced-motion.
<script>
import { reveal } from 'svelte-weave';
</script>
<div use:reveal>Fades in on scroll (default: fadeIn)</div>
<div use:reveal={{ preset: 'slideUp', delay: 150 }}>Slides up with 150 ms delay</div>
<div use:reveal={{ preset: 'scaleIn', once: false }}>Re-animates on every re-entry</div>
Built-in reveal presets:
| Preset | Effect |
|---|---|
fadeIn (default) |
Opacity 0 → 1 |
slideUp |
Slides up + fade |
slideDown |
Slides down + fade |
slideLeft |
Slides in from right + fade |
slideRight |
Slides in from left + fade |
scaleIn |
Scale 0.92 → 1 + fade |
blurIn |
Blur → sharp + fade |
Options:
| Option | Default | Description |
|---|---|---|
preset |
'fadeIn' |
Which animation to use |
delay |
0 |
Milliseconds before the animation starts |
once |
true |
Whether to animate only the first time |
threshold |
0.15 |
How much of the element must be visible (0–1) |
use:clickEffect)Short feedback animation triggered on every click. Works on any element or via the <Button clickEffect="..."> prop.
<script>
import { clickEffect } from 'svelte-weave';
import { Button } from 'svelte-weave';
</script>
<button use:clickEffect="ripple">Material ripple</button>
<Button clickEffect="bounce">Bounce on click</Button>
Built-in click presets:
| Preset | Effect |
|---|---|
pulse |
Quick compress + release |
bounce |
Compress → overshoot → settle |
shake |
Horizontal shake |
ripple |
Material-style ripple from cursor position |
import { registerRevealPreset, registerClickPreset } from 'svelte-weave';
// 1. Define your @keyframes + class in your own CSS
// 2. Register the name → class mapping:
registerRevealPreset('spinIn', { className: 'my-anim-spin' });
registerClickPreset('flash', { className: 'my-click-flash' });
// 3. Use it:
// <div use:reveal={{ preset: 'spinIn' }}>...</div>
// <Button clickEffect="flash">Flash</Button>
:root {
--sw-anim-duration: 400ms;
--sw-anim-easing: cubic-bezier(0.4, 0, 0.2, 1);
--sw-anim-delay: 0ms;
--sw-anim-distance: 1.5rem;
--sw-anim-scale-from: 0.92;
--sw-anim-blur-amount: 8px;
}
/* Stagger a list of cards */
.card:nth-child(1) { --sw-anim-delay: 0ms; }
.card:nth-child(2) { --sw-anim-delay: 100ms; }
.card:nth-child(3) { --sw-anim-delay: 200ms; }
Components that accept icons (Button, Input, etc.) take an IconInput — any of three forms:
| Form | Example |
|---|---|
| SVG string | "<svg viewBox='0 0 24 24'>...</svg>" |
| Registered name | "mdi:home" |
| Svelte component | import HomeIcon from './HomeIcon.svelte' |
<!-- SVG string inline -->
<Button iconLeft="<svg viewBox='0 0 24 24' fill='currentColor'><path d='M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z'/></svg>">
Home
</Button>
<!-- Registered icon name (needs a resolver, see below) -->
<Button iconLeft="mdi:home" iconRight="mdi:arrow-right">Go Home</Button>
<!-- Svelte component -->
<Button iconLeft={HomeIcon}>Home</Button>
<!-- Standalone icon -->
<WeaveIcon icon="mdi:star" size={24} />
<WeaveIcon icon={StarIcon} class="text-yellow-500" />
Call this once at app startup (e.g. in your root +layout.svelte):
import { registerIconResolver } from 'svelte-weave';
const myIcons: Record<string, string> = { 'my:home': '<svg>...</svg>' };
registerIconResolver((name) => myIcons[name] ?? null);
With Iconify:
import { registerIconResolver } from 'svelte-weave';
import { getIcon, renderSVG } from '@iconify/utils';
import icons from '@iconify-json/mdi/icons.json';
registerIconResolver((name) => {
if (!name.startsWith('mdi:')) return null;
const data = getIcon(icons, name.slice(4));
return data ? renderSVG(data) : null;
});
If your project uses Tailwind, a single plugin import maps your Tailwind color theme to --sw-* vars automatically.
// tailwind.config.js
import { weavePlugin } from 'svelte-weave/tailwind';
export default {
plugins: [weavePlugin],
};
import { createWeavePlugin } from 'svelte-weave/tailwind';
export default {
plugins: [
createWeavePlugin({
primaryColor: 'violet',
dangerColor: 'rose',
successColor: 'emerald',
warningColor: 'orange',
}),
],
};
cn(...classes) — class merge helperMerges class strings, filtering falsy values. Useful for consumer-side class composition.
import { cn } from 'svelte-weave';
const cls = cn('base-class', condition && 'conditional', userClass);
use:clickOutside — click-outside Svelte actionDispatches a clickoutside event when the user clicks outside the element. Useful for closing dropdowns or menus without a full <Modal>.
<script>
import { clickOutside } from 'svelte-weave';
let open = true;
</script>
<div use:clickOutside on:clickoutside={() => open = false}>
<!-- dropdown content -->
</div>
svelte-weave → components, theme system, icon system, animations, utilities
svelte-weave/tailwind → Tailwind CSS plugin
svelte-weave/styles → CSS custom properties + base reset
# Start the component playground
npm run dev
# TypeScript + Svelte type checking
npm run check
# Build the publishable package → ./dist/
npm run package
# Build the SvelteKit demo app
npm run build
Compatible with npm, pnpm, yarn, and bun.
MIT © svelte-weave contributors