
Svelte Template Directives
- 95 installs
- 92 repo stars
- Updated April 29, 2026
- spences10/svelte-skills-kit
Helps with ai & agent building tasks.
About
svelte-template-directives is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- svelte-template-directives
- AI & Agent Building
- AI-coding skill
Svelte Template Directives by the numbers
- 95 all-time installs (skills.sh)
- +9 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #4,606 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spences10/svelte-skills-kit --skill svelte-template-directivesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 95 |
|---|---|
| repo stars | ★ 92 |
| Last updated | April 29, 2026 |
| Repository | spences10/svelte-skills-kit ↗ |
What it does
Helps with ai & agent building tasks.
Files
Svelte Template Directives
@attach (Svelte 5.29+)
The reactive alternative to `use:` actions. Re-runs when dependencies change, passes through components via spread, supports cleanup functions.
<script>
import ImageZoom from 'js-image-zoom';
function useZoom(options) {
return (element) => {
new ImageZoom(element, options);
return () => console.log('cleanup');
};
}
</script>
<!-- Re-runs if options changes (use: wouldn't!) -->
<figure {@attach useZoom({ width: 400 })}>
<img src="photo.jpg" alt="zoomable" />
</figure>Quick Reference
| Directive | Purpose | Reactive? |
|---|---|---|
{@attach} | DOM manipulation, 3rd-party | Yes |
{@html} | Render raw HTML strings | Yes |
{@render} | Render snippets | Yes |
{@const} | Local constants in blocks | N/A |
{@debug} | Pause debugger on value change | N/A |
{#each (key)} | Keyed iteration (always key!) | Yes |
<svelte:window> | Window event listeners | N/A |
@attach vs use: Actions
| Feature | use: | @attach |
|---|---|---|
| Re-runs on arg change | No | Yes |
| Composable | Limited | Fully |
| Pass through props | Manual | Auto via spread |
| Convert legacy | N/A | fromAction() |
Reference Files
- attach-patterns.md - Real-world @attach
examples
- other-directives.md - @html, @render,
@const, @debug
Notes
@attachrequires Svelte 5.29+- Use
fromActionfromsvelte/attachmentsto convert legacy actions - Attachments pass through wrapper components when you spread props
- Always use keyed each blocks — never use index as key
- Use
<svelte:window>/<svelte:document>for global events, not$effect - Last verified: 2026-03-12
<!-- PROGRESSIVE DISCLOSURE GUIDELINES:
- Keep this file ~50 lines total (max ~150 lines)
- Use 1-2 code blocks only (recommend 1)
- Keep description <200 chars for Level 1 efficiency
- Move detailed docs to references/ for Level 3 loading
- This is Level 2 - quick reference ONLY, not a manual
-->
@attach Patterns
Available in Svelte 5.29+
Why @attach Over use: Actions?
Attachments are fully reactive. When dependencies change, the attachment re-runs automatically. Actions don't do this - they only run once on mount.
<!-- use: - runs ONCE, ignores content changes -->
<button use:tooltip={content}>Won't update</button>
<!-- @attach - re-runs when content changes -->
<button {@attach tooltip(content)}>Updates!</button>Pattern 1: Third-Party Library Integration
The most common use case - integrating DOM-manipulating libraries like image zoom, tooltips, editors, etc.
ImageZoom Example
<script>
import ImageZoom from 'js-image-zoom';
const options = { width: 400, zoomWidth: 500 };
function useZoom(dom) {
new ImageZoom(dom, options);
return () => {
console.log('cleaning up');
};
}
</script>
<figure {@attach useZoom}>
<img src="photo.jpg" alt="zoomable photo" />
</figure>Tippy.js Tooltips
<script>
import tippy from 'tippy.js';
function tooltip(content) {
return (element) => {
const instance = tippy(element, { content });
return instance.destroy;
};
}
let tip = $state('Hello!');
</script>
<!-- Tooltip content updates reactively -->
<button {@attach tooltip(tip)}>Hover me</button>
<input bind:value={tip} placeholder="Change tooltip" />Pattern 2: Canvas Drawing
Perfect for canvas where you need reactive updates without recreating the context.
<script>
let color = $state('#ff0000');
let size = $state(50);
</script>
<canvas
width="200"
height="200"
{@attach (canvas) => {
const ctx = canvas.getContext('2d');
// This inner effect re-runs on color/size change
// but canvas context is preserved
$effect(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = color;
ctx.fillRect(
(canvas.width - size) / 2,
(canvas.height - size) / 2,
size,
size
);
});
}}
/>
<input type="color" bind:value={color} />
<input type="range" bind:value={size} min="10" max="100" />Pattern 3: Component Pass-Through
Attachments automatically pass through wrapper components when you spread props. This enables "augmented element" patterns.
<!-- Button.svelte -->
<script>
let { children, ...props } = $props();
</script>
<button {...props}>
{@render children?.()}
</button><!-- App.svelte -->
<script>
import Button from './Button.svelte';
import { tooltip } from './attachments.js';
</script>
<!-- The attachment passes through to the inner <button>! -->
<Button {@attach tooltip('Click me for help')}>
Help
</Button>Pattern 4: Attachment Factories
Factory functions return attachment implementations, enabling parameterized behavior.
<script>
function highlight(color) {
return (element) => {
const original = element.style.backgroundColor;
element.style.backgroundColor = color;
return () => {
element.style.backgroundColor = original;
};
};
}
let isActive = $state(false);
</script>
<!-- Attachment recreates when isActive changes -->
{#if isActive}
<div {@attach highlight('yellow')}>Highlighted!</div>
{/if}Pattern 5: Avoiding Expensive Re-runs
For expensive setup work, pass data via accessor function and read it in a child effect.
<script>
function expensiveChart(getData) {
return (node) => {
// Expensive - runs ONCE
const chart = createComplexChart(node);
// Cheap - re-runs on data change
$effect(() => {
chart.update(getData());
});
return () => chart.destroy();
};
}
let data = $state([1, 2, 3]);
</script>
<!-- Pass accessor function, not the data directly -->
<div {@attach expensiveChart(() => data)}>Chart</div>Pattern 6: Converting Legacy Actions
Use fromAction to convert existing action libraries to attachments.
<script>
import { fromAction } from 'svelte/attachments';
import { someAction } from 'some-legacy-library';
const attached = fromAction(someAction);
</script>
<!-- Now works as an attachment with full reactivity -->
<div {@attach attached(options)}>...</div>Pattern 7: Multiple Attachments
Elements can have any number of attachments.
<button
{@attach tooltip('Help text')}
{@attach trackClicks}
{@attach highlight(isActive ? 'yellow' : 'transparent')}
>
Multi-attached button
</button>Pattern 8: Inline Attachments
For one-off cases, define attachments inline.
<div
{@attach (el) => {
console.log('mounted:', el);
return () => console.log('unmounted');
}}
>
Lifecycle logging
</div>Pattern 9: DOM-Controlling Libraries (ProseMirror, etc.)
For libraries that want to control their own DOM segment, combine @attach with the imperative component API.
<script>
import { mount, unmount } from 'svelte';
import MyComponent from './MyComponent.svelte';
function proseMirrorNodeView(node) {
return (dom) => {
// ProseMirror controls this DOM node
// but we can mount Svelte components inside it
const component = mount(MyComponent, {
target: dom,
props: { data: node.attrs }
});
return () => unmount(component);
};
}
</script>Pattern 10: Registering Elements with Global State
Use @attach to register DOM elements with state classes. This avoids $effect sync loops and is cleaner than bind:this chains.
// modal-state.svelte.ts
class ModalState {
dialog: HTMLDialogElement | null = null;
input: HTMLInputElement | null = null;
is_open = $state(false);
// Attach functions return cleanup
register = (el: HTMLDialogElement) => {
this.dialog = el;
return () => {
this.dialog = null;
};
};
register_input = (el: HTMLInputElement) => {
this.input = el;
return () => {
this.input = null;
};
};
open() {
if (!this.dialog?.open) {
this.is_open = true;
this.dialog?.showModal();
this.input?.focus();
}
}
close() {
this.is_open = false;
this.dialog?.close();
}
toggle() {
this.is_open ? this.close() : this.open();
}
}
export const modal_state = new ModalState();<!-- Modal.svelte -->
<script>
import { modal_state } from './modal-state.svelte';
</script>
<dialog
{@attach modal_state.register}
onclose={modal_state.close}
>
<input {@attach modal_state.register_input} />
</dialog><!-- Anywhere else - no component ref needed -->
<script>
import { modal_state } from './modal-state.svelte';
</script>
<button onclick={modal_state.toggle}>Open Modal</button>Benefits:
- No $effect needed for state/DOM sync
- State controls element directly via imperative methods
- Clean cleanup on unmount
- Any component can open/close without bind:this chains
- Avoids event loops from
dialog.close()firingonclose
When to Still Use use: Actions
- Legacy code/libraries not yet updated
- When you specifically DON'T want re-runs on argument change
- Simple one-time DOM setup with no reactive dependencies
Other Template Directives
{@html ...}
Renders raw HTML strings. Use with caution - never render untrusted content.
<script>
let htmlContent = '<strong>Bold</strong> and <em>italic</em>';
</script>
{@html htmlContent}Security Warning
Always sanitize user-provided HTML:
<script>
import DOMPurify from 'dompurify';
let userContent = $state('');
const sanitized = $derived(DOMPurify.sanitize(userContent));
</script>
{@html sanitized}Common Use Cases
- Rendering markdown converted to HTML
- CMS content with formatting
- Syntax-highlighted code blocks
{@render ...}
Renders snippets - Svelte 5's replacement for slots.
<script>
let { header, children } = $props();
</script>
<div class="card">
{#if header}
<header>{@render header()}</header>
{/if}
<main>{@render children?.()}</main>
</div>With Parameters
Snippets can receive parameters:
<script>
let { row } = $props();
let items = $state([{ name: 'Apple' }, { name: 'Banana' }]);
</script>
{#each items as item}
{@render row(item)}
{/each}<!-- Usage -->
<List>
{#snippet row(item)}
<li>{item.name}</li>
{/snippet}
</List>Optional Snippets
Use optional chaining for optional snippets:
{@render footer?.()}{@const ...}
Declares local constants within template blocks. Useful in {#each} and {#if}.
{#each items as item}
{@const fullName = `${item.firstName} ${item.lastName}`}
{@const isLongName = fullName.length > 20}
<div class:truncate={isLongName}>
{fullName}
</div>
{/each}Why Use @const?
- Avoids recalculating values multiple times in a block
- Makes complex expressions more readable
- Scoped to the block - doesn't pollute component scope
{@debug ...}
Pauses execution and opens devtools when specified values change.
<script>
let count = $state(0);
let items = $state([]);
</script>
{@debug count, items}
<button onclick={() => count++}>Increment</button>Tips
- Remove
{@debug}before production - Use with specific variables, not entire objects
- Combine with browser devtools for best debugging experience
Debug Without Variables
{@debug}Pauses on every update (rarely useful, but available).
Keyed Each Blocks
Per official Svelte best practices: always use keyed each blocks for better performance.
Basic Keyed Each
{#each items as item (item.id)}
<div>{item.name}</div>
{/each}Key must uniquely identify the object. Do NOT use the index:
<!-- WRONG - index as key -->
{#each items as item, i (i)}
<div>{item.name}</div>
{/each}
<!-- RIGHT - unique identifier -->
{#each items as item (item.id)}
<div>{item.name}</div>
{/each}Why Keys Matter
Without keys, Svelte updates existing DOM nodes in place when the array changes. With keys, Svelte can surgically insert, remove, or reorder items instead.
Without key: Removing item 2 from [A, B, C] updates node 2 to show C's data and removes the last node.
With key: Removing item B actually removes B's DOM node, leaving A and C untouched.
Avoid Destructuring with bind
Per best practices: avoid destructuring if you need to mutate the item.
<!-- WRONG - destructured value is disconnected from original -->
{#each items as { count } (item.id)}
<input bind:value={count} /> <!-- Won't update the original item -->
{/each}
<!-- RIGHT - reference the item directly -->
{#each items as item (item.id)}
<input bind:value={item.count} /> <!-- Updates the original -->
{/each}Window and Document Events
Per official best practices: use <svelte:window> and <svelte:document> for window/document event listeners. Avoid onMount or $effect for this.
<svelte:window onkeydown={handleKeydown} onscroll={handleScroll} />
<svelte:document onvisibilitychange={handleVisibility} />Common Events
<!-- Keyboard shortcuts -->
<svelte:window onkeydown={(e) => {
if (e.key === 'Escape') closeModal();
if (e.ctrlKey && e.key === 's') { e.preventDefault(); save(); }
}} />
<!-- Online/offline detection -->
<svelte:window ononline={() => status = 'online'} onoffline={() => status = 'offline'} />
<!-- Responsive design -->
<svelte:window
bind:innerWidth={width}
bind:innerHeight={height}
/>Bindable Window Properties
<svelte:window
bind:innerWidth
bind:innerHeight
bind:scrollX
bind:scrollY
bind:online
/>Why not $effect: <svelte:window> automatically cleans up listeners when the component is destroyed. No manual cleanup needed.