svelte
Skill50 starsChanged 11 months agoMITView on GitHub
Builds token-driven Svelte 5 components with runes ($state, $props) and TypeScript. Use when creating Svelte component libraries, integrating design tokens, or building SvelteKit design system components.
9 other projects use a version of this.
Adopted by
Most recently taken up by
- vercel-labs/json-rendertheir own version, first seen Oct 29, 2026
- G1Joshi/Agent-Skillstheir own version, first seen Oct 13, 2026
- Mindrally/skillstheir own version, first seen Oct 8, 2026
- epicenter-md/epicentertheir own version, first seen Oct 8, 2026
- EpicenterHQ/epicentertheir own version, first seen Oct 8, 2026
- conjure-3301/skillstheir own version, first seen Aug 17, 2026
Other versions9 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 9
- vercel-labs/json-render★ 19ksame name
- epicenter-md/epicenter★ 4.8ksame name
- EpicenterHQ/epicenter★ 4.8ksame name
- Mindrally/skills★ 275same name
- G1Joshi/Agent-Skills★ 15same name
- ffsshhttiikk/opencode-agents-skills★ 2same name
- hawk-digital-environments/ai-skills★ 1same name
- conjure-3301/skills★ 0same name
- mae616/ai-tech-knowledge★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository29 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- aria-patternsSkill
- color-contrastSkill
- focus-statesSkill
- component-docsSkill
- token-docsSkill
- angularSkill
- reactSkill
- vueSkill
- getting-startedSkill
- animation-principlesSkill
- compound-componentsSkill
- dark-modeSkill
- icon-systemSkill
- layout-primitivesSkill
- breakpointsSkill
- color-scaleSkill
- design-tokens-structureSkill
- motion-scaleSkill
- radius-scaleSkill
- responsive-typographySkill
- shadow-scaleSkill
- spacing-scaleSkill
- type-scaleSkill
- z-index-scaleSkill
- figmaSkill
- framerSkill
- storybookSkill
- style-dictionarySkill
The file
We split this skill into the rules it gives an agent
2 rules. 0 are common, 0 are distinctive, and 2 are very specific to this project.
0
Common
Most of the 8 edited versions we read keep these.
0
Distinctive
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 23
Define props with TypeScript: Use $props() rune
- Very specific: line 26
Extract state logic: Use $state, $derived, and $effect
“Common” is measured: kept by at least 6 in 10 of the 8 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Svelte Component Patterns
Overview
Build accessible, token-driven Svelte 5 components using runes and modern patterns. Covers component structure, TypeScript integration, stores, and consuming design tokens.
When to Use
- Creating a Svelte component library
- Building components that use design tokens
- Setting up a design system in SvelteKit
- Converting designs to Svelte components
The Process
- Identify component type: Primitive, composite, or layout?
- Choose styling approach: Scoped CSS, Tailwind, or global tokens?
- Define props with TypeScript: Use
$props()rune - Implement with tokens: CSS custom properties
- Add accessibility: ARIA, keyboard handling, focus management
- Extract state logic: Use
$state,$derived, and$effect
Svelte 5 Runes Quick Reference
| Rune | Purpose | Example |
$state | Reactive state | let count = $state(0) |
$derived | Computed values | let double = $derived(count * 2) |
$effect | Side effects | $effect(() => console.log(count)) |
$props | Component props | let { variant = 'primary' } = $props() |
$bindable | Two-way binding | let { value = $bindable() } = $props() |
Project Structure
src/
├── lib/
│ ├── components/
│ │ ├── primitives/
│ │ │ ├── Button/
│ │ │ │ ├── Button.svelte
│ │ │ │ ├── Button.test.ts
│ │ │ │ └── index.ts
│ │ │ ├── Input/
│ │ │ └── Text/
│ │ ├── composite/
│ │ │ ├── Card/
│ │ │ ├── Modal/
│ │ │ └── Dropdown/
│ │ └── layout/
│ │ ├── Stack/
│ │ ├── Grid/
│ │ └── Container/
│ ├── stores/
│ │ └── theme.svelte.ts
│ ├── tokens/
│ │ └── index.css
│ └── index.ts
└── routes/Component Patterns
Button Component
Button.svelte:
<script lang="ts">
import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements';
interface Props extends HTMLButtonAttributes {
/** Visual style variant */
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
/** Size of the button */
size?: 'sm' | 'md' | 'lg';
/** Full width button */
fullWidth?: boolean;
/** Loading state */
loading?: boolean;
/** Icon before text */
leftIcon?: Snippet;
/** Icon after text */
rightIcon?: Snippet;
/** Button content */
children: Snippet;
}
let {
variant = 'primary',
size = 'md',
fullWidth = false,
loading = false,
disabled = false,
leftIcon,
rightIcon,
children,
class: className = '',
...restProps
}: Props = $props();
let isDisabled = $derived(disabled || loading);
</script>
<button
class="btn btn--{variant} btn--{size} {className}"
class:btn--full-width={fullWidth}
class:btn--loading={loading}
disabled={isDisabled}
aria-busy={loading}
{...restProps}
>
{#if loading}
<span class="btn__spinner" aria-hidden="true"></span>
{/if}
{#if leftIcon}
<span class="btn__icon">
{@render leftIcon()}
</span>
{/if}
<span class="btn__label">
{@render children()}
</span>
{#if rightIcon}
<span class="btn__icon">
{@render rightIcon()}
</span>
{/if}
</button>
<style>
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
font-family: inherit;
font-weight: 500;
line-height: 1;
white-space: nowrap;
cursor: pointer;
user-select: none;
border: 1px solid transparent;
border-radius: var(--radius-md);
transition:
background-color 150ms ease,
border-color 150ms ease,
transform 100ms ease;
}
.btn:focus-visible {
outline: 2px solid var(--color-primary-500);
outline-offset: 2px;
}
.btn:active:not(:disabled) {
transform: scale(0.98);
}
.btn:disabled {
cursor: not-allowed;
opacity: 0.5;
}
/* Variants */
.btn--primary {
background-color: var(--color-primary-500);
color: white;
}
.btn--primary:hover:not(:disabled) {
background-color: var(--color-primary-600);
}
.btn--secondary {
background-color: transparent;
border-color: var(--color-gray-300);
color: var(--color-gray-700);
}
.btn--secondary:hover:not(:disabled) {
background-color: var(--color-gray-50);
border-color: var(--color-gray-400);
}
.btn--ghost {
background-color: transparent;
color: var(--color-gray-700);
}
.btn--ghost:hover:not(:disabled) {
background-color: var(--color-gray-100);
}
.btn--danger {
background-color: var(--color-error-500);
color: white;
}
.btn--danger:hover:not(:disabled) {
background-color: var(--color-error-600);
}
/* Sizes */
.btn--sm {
height: 32px;
padding: 0 var(--spacing-sm);
font-size: var(--text-sm);
}
.btn--md {
height: 40px;
padding: 0 var(--spacing-md);
font-size: var(--text-base);
}
.btn--lg {
height: 48px;
padding: 0 var(--spacing-lg);
font-size: var(--text-lg);
}
/* Modifiers */
.btn--full-width {
width: 100%;
}
.btn--loading .btn__label {
opacity: 0;
}
.btn__spinner {
position: absolute;
width: 1em;
height: 1em;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 600ms linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.btn__icon {
display: flex;
flex-shrink: 0;
}
</style>Input Component
Input.svelte:
<script lang="ts">
import type { HTMLInputAttributes } from 'svelte/elements';
interface Props extends Omit<HTMLInputAttributes, 'size'> {
/** Bound value */
value?: string;
/** Label text */
label?: string;
/** Helper text */
helperText?: string;
/** Error message */
error?: string;
/** Size variant */
size?: 'sm' | 'md' | 'lg';
/** Full width */
fullWidth?: boolean;
}
let {
value = $bindable(''),
label,
helperText,
error,
size = 'md',
fullWidth = false,
disabled = false,
required = false,
id,
class: className = '',
...restProps
}: Props = $props();
let inputId = $derived(id || `input-${crypto.randomUUID().slice(0, 8)}`);
let helperId = $derived(`${inputId}-helper`);
let errorId = $derived(`${inputId}-error`);
let inputRef: HTMLInputElement;
export function focus() {
inputRef?.focus();
}
</script>
<div class="input-container" class:input-container--full-width={fullWidth}>
{#if label}
<label for={inputId} class="input-label">
{label}
{#if required}
<span class="input-required" aria-hidden="true">*</span>
{/if}
</label>
{/if}
<div
class="input-wrapper input-wrapper--{size} {className}"
class:input-wrapper--error={error}
class:input-wrapper--disabled={disabled}
>
{#if $$slots.startAdornment}
<span class="input-adornment">
<slot name="startAdornment" />
</span>
{/if}
<input
bind:this={inputRef}
bind:value
{id}
{disabled}
{required}
aria-invalid={!!error}
aria-describedby={error ? errorId : helperText ? helperId : undefined}
class="input"
{...restProps}
/>
{#if $$slots.endAdornment}
<span class="input-adornment">
<slot name="endAdornment" />
</span>
{/if}
</div>
{#if error}
<span id={errorId} class="input-error" role="alert">{error}</span>
{:else if helperText}
<span id={helperId} class="input-helper">{helperText}</span>
{/if}
</div>
<style>
.input-container {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.input-container--full-width {
width: 100%;
}
.input-label {
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-gray-700);
}
.input-required {
color: var(--color-error-500);
margin-left: 2px;
}
.input-wrapper {
display: flex;
align-items: center;
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-md);
background-color: white;
transition: border-color 150ms ease, box-shadow 150ms ease;
}
.input-wrapper:focus-within {
border-color: var(--color-primary-500);
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.15);
}
.input-wrapper--error {
border-color: var(--color-error-500);
}
.input-wrapper--error:focus-within {
box-shadow: 0 0 0 3px rgb(239 68 68 / 0.15);
}
.input-wrapper--disabled {
background-color: var(--color-gray-100);
cursor: not-allowed;
}
.input-wrapper--sm {
height: 32px;
padding: 0 var(--spacing-sm);
}
.input-wrapper--md {
height: 40px;
padding: 0 var(--spacing-md);
}
.input-wrapper--lg {
height: 48px;
padding: 0 var(--spacing-md);
}
.input {
flex: 1;
width: 100%;
border: none;
background: transparent;
font: inherit;
color: var(--color-gray-900);
}
.input:focus {
outline: none;
}
.input::placeholder {
color: var(--color-gray-400);
}
.input:disabled {
cursor: not-allowed;
color: var(--color-gray-500);
}
.input-adornment {
display: flex;
align-items: center;
color: var(--color-gray-500);
}
.input-helper {
font-size: var(--text-sm);
color: var(--color-gray-500);
}
.input-error {
font-size: var(--text-sm);
color: var(--color-error-500);
}
</style>Stack Layout Component
Stack.svelte:
<script lang="ts">
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
type SpacingToken = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
interface Props extends HTMLAttributes<HTMLElement> {
/** Direction of stacking */
direction?: 'row' | 'column';
/** Gap between items */
gap?: SpacingToken;
/** Horizontal alignment */
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';
/** Distribution */
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
/** Wrap items */
wrap?: boolean;
/** Full width */
fullWidth?: boolean;
/** Content */
children: Snippet;
}
let {
direction = 'column',
gap = 'md',
align = 'stretch',
justify = 'start',
wrap = false,
fullWidth = false,
children,
class: className = '',
...restProps
}: Props = $props();
const alignMap: Record<string, string> = {
start: 'flex-start',
center: 'center',
end: 'flex-end',
stretch: 'stretch',
baseline: 'baseline',
};
const justifyMap: Record<string, string> = {
start: 'flex-start',
center: 'center',
end: 'flex-end',
between: 'space-between',
around: 'space-around',
evenly: 'space-evenly',
};
let styles = $derived(`
--stack-gap: var(--spacing-${gap});
--stack-align: ${alignMap[align]};
--stack-justify: ${justifyMap[justify]};
`);
</script>
<div
class="stack stack--{direction} {className}"
class:stack--wrap={wrap}
class:stack--full-width={fullWidth}
style={styles}
{...restProps}
>
{@render children()}
</div>
<style>
.stack {
display: flex;
gap: var(--stack-gap, var(--spacing-md));
align-items: var(--stack-align, stretch);
justify-content: var(--stack-justify, flex-start);
}
.stack--column {
flex-direction: column;
}
.stack--row {
flex-direction: row;
}
.stack--wrap {
flex-wrap: wrap;
}
.stack--full-width {
width: 100%;
}
</style>State Management with Runes
Theme Store
theme.svelte.ts:
type Theme = 'light' | 'dark' | 'system';
class ThemeState {
theme = $state<Theme>('system');
resolvedTheme = $state<'light' | 'dark'>('light');
constructor() {
if (typeof window !== 'undefined') {
const stored = localStorage.getItem('theme') as Theme | null;
if (stored) this.theme = stored;
this.updateResolved();
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', () => this.updateResolved());
}
}
private updateResolved() {
if (this.theme === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
this.resolvedTheme = prefersDark ? 'dark' : 'light';
} else {
this.resolvedTheme = this.theme;
}
document.documentElement.dataset.theme = this.resolvedTheme;
}
setTheme(newTheme: Theme) {
this.theme = newTheme;
localStorage.setItem('theme', newTheme);
this.updateResolved();
}
}
export const themeState = new ThemeState();Usage:
<script>
import { themeState } from '$lib/stores/theme.svelte';
</script>
<p>Current theme: {themeState.resolvedTheme}</p>
<button onclick={() => themeState.setTheme('dark')}>Dark Mode</button>Context Pattern
ThemeProvider.svelte:
<script lang="ts">
import { setContext } from 'svelte';
import { themeState } from '$lib/stores/theme.svelte';
setContext('theme', themeState);
</script>
<slot />Consuming context:
<script>
import { getContext } from 'svelte';
const theme = getContext('theme');
</script>Actions (Directives)
clickOutside.ts:
export function clickOutside(node: HTMLElement, callback: () => void) {
function handleClick(event: MouseEvent) {
if (!node.contains(event.target as Node)) {
callback();
}
}
document.addEventListener('click', handleClick, true);
return {
destroy() {
document.removeEventListener('click', handleClick, true);
},
};
}Usage:
<script>
import { clickOutside } from '$lib/actions/clickOutside';
let open = $state(false);
</script>
<div use:clickOutside={() => open = false}>
Dropdown content
</div>focusTrap.ts:
export function focusTrap(node: HTMLElement) {
const focusable = node.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
function handleKeydown(e: KeyboardEvent) {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
first?.focus();
node.addEventListener('keydown', handleKeydown);
return {
destroy() {
node.removeEventListener('keydown', handleKeydown);
},
};
}Transitions
Custom transition with tokens:
<script>
import { fly, fade } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
let visible = $state(true);
</script>
{#if visible}
<div
transition:fly={{ y: 10, duration: 200, easing: cubicOut }}
class="modal"
>
Modal content
</div>
{/if}Library Export
index.ts:
// Components
export { default as Button } from './components/primitives/Button/Button.svelte';
export { default as Input } from './components/primitives/Input/Input.svelte';
export { default as Stack } from './components/layout/Stack/Stack.svelte';
export { default as Card } from './components/composite/Card/Card.svelte';
// Stores
export { themeState } from './stores/theme.svelte';
// Actions
export { clickOutside } from './actions/clickOutside';
export { focusTrap } from './actions/focusTrap';
// Types
export type { ButtonProps } from './components/primitives/Button/Button.svelte';Testing
Button.test.ts:
import { render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import Button from './Button.svelte';
describe('Button', () => {
it('renders children', () => {
render(Button, {
props: { children: () => 'Click me' },
});
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
it('handles click events', async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(Button, {
props: { onclick: onClick, children: () => 'Click' },
});
await user.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledOnce();
});
it('is disabled when loading', () => {
render(Button, {
props: { loading: true, children: () => 'Loading' },
});
expect(screen.getByRole('button')).toBeDisabled();
});
it('applies variant class', () => {
render(Button, {
props: { variant: 'danger', children: () => 'Delete' },
});
expect(screen.getByRole('button')).toHaveClass('btn--danger');
});
});Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

