This skill covers Svelte 5 template syntax, including basic markup, block-level expressions, template tags, event handling, attribute binding, style/class directives, attachments/actions, transitions and animations.
When to use this skill
Use this skill when the user needs to write or understand template syntax in Svelte components, including scenarios such as conditional rendering, list rendering, async handling, snippet reuse, event binding, two-way data binding, transition animations, etc.
Critical: Basic Markup
Element tags
- Lowercase tags (
<div>) = HTML elements - Uppercase/dot tags (
<Widget>、<my.stuff>) = components
Attribute binding
<!-- 布尔属性 -->
<button disabled={!clickable}>...</button>
<input required={false} />
<!-- 简写(同名字相同) -->
<button {disabled}>...</button>
<!-- 展开属性(顺序决定优先级:后写覆盖先写) -->
<Widget {...props} a="b" />
<!-- 布尔属性:truthy 包含,falsy 排除 -->
<div data-active={isActive}> <!-- true → 包含,false/null/undefined → 排除 -->
Event handling
<!-- onclick 是属性,不是指令 -->
<button onclick={() => count++}>+</button>
<input {onkeydown} /> <!-- 简写形式 -->
<button {...handlerProps}> <!-- 展开形式 -->
<!-- 事件委托:大多数事件委托到根节点,无需 stopPropagation -->
<!-- 若需阻止:使用 svelte/events 的 on 函数 -->
Text expressions
<p>{expression}</p> <!-- null/undefined → 省略,其余转字符串 -->
<p>Hello {name}!</p>
<!-- 原始 HTML(注意 XSS) -->
{@html rawContent}
<!-- 转义花括号 -->
<p>使用 { 和 }</p>
Critical: {#if ...}
Conditional rendering:
{#if count > 10}
<p>big</p>
{:else if count > 5}
<p>medium</p>
{:else}
<p>small</p>
{/if}
{:else if} can be chained multiple times,{:else} is the final fallback.
Critical: {#each ...}
List rendering:
{#each items as item}
<li>{item.name}</li>
{/each}
<!-- 带索引 -->
{#each items as item, i}
<li>{i + 1}: {item.name}</li>
{/each}
<!-- 空列表 fallback -->
{#each todos as todo}
<p>{todo.text}</p>
{:else}
<p>No tasks!</p>
{/each}
Efficient updates with keys
{#each items as item (item.id)}
<li>{item.name}</li>
{/each}
The key must be a unique identifier (string/number), used by Svelte for efficient diffing and DOM updates (insert/move/delete instead of full re-rendering).
Destructuring and rest
{#each items as { id, name, ...rest }}
<li><span>{id}</span><MyComponent {...rest} /></li>
{/each}
{#each items as [id, ...values]}
<li><span>{id}</span></li>
{/each}
Render N times
{#each { length: 8 }, rank}
<div class:black={(rank) % 2 === 1}></div>
{/each}
Critical: {#key ...}
Destroy and rebuild content when the expression changes (triggering transition animations):
{#key value}
<div transition:fade>{value}</div>
{/key}
Critical: {#await ...}
Promise async state branches:
{#await promise}
<p>loading...</p>
{:then value}
<p>result: {value}</p>
{:catch error}
<p>error: {error.message}</p>
{/await}
<!-- 简洁形式(无 pending UI) -->
{#await promise then value}
<p>{value}</p>
{/await}
<!-- 仅错误处理 -->
{#await promise catch error}
<p>{error.message}</p>
{/await}
Lazy-loaded components
{#await import('./Heavy.svelte') then { default: Component }}
<Component />
{/await}
await expressions (Svelte 5.36+ experimental)
<!-- 需 svelte.config.js 中启用 experimental.async -->
<svelte:boundary>
<p>{await fetchData()}</p>
{#snippet pending()}<p>loading</p>{/snippet}
</svelte:boundary>
Features: synchronous updates, concurrent execution (independent await)、<svelte:boundary> pending snippet、fork() API。
Critical: {#snippet ...}
Reusable markup snippets, replacing Svelte 4's Slots:
{#snippet card(item)}
<div class="card">
<h3>{item.title}</h3>
<p>{item.body}</p>
</div>
{/snippet}
{@render card(item)}
Snippet scope
Snippets can reference external variables (script variables or {#each} block-level variables), and are visible to siblings/child nodes ofthe same lexical scope:
{#each items as item}
{#snippet itemCard()}
<div>{item.name}</div> <!-- 引用 item -->
{/snippet}
{@render itemCard()}
{/each}
Explicit vs implicit prop
<!-- 隐式 prop(推荐)-->
<Table {data}>
{#snippet header()}
<th>Name</th>
{/snippet}
</Table>
<!-- 显式 prop -->
<Table {data} header={myHeader} />
Implicit children
Non-snippet content within a component tag automatically becomes children snippet:
<Button>click me</Button>
<!-- Button.svelte -->
<script>
let { children } = $props();
</script>
<button>{@render children()}</button>
Optional snippet
{@render children?.()} <!-- 可选链 -->
{#if children}{:else}fallback{/if} <!-- #if fallback -->
Snippet type definition
<script lang="ts">
import type { Snippet } from 'svelte';
let { row }: { row: Snippet<[Item]> } = $props();
</script>
Exported snippet (5.5+)
<script module>
export { mySnippet };
</script>
{#snippet mySnippet()}
<div>content</div>
{/snippet}
Programmatic creation (createRawSnippet)
import { createRawSnippet } from 'svelte';
const greet = createRawSnippet<[string]>((name) => ({
render: () => `<p>Hello, ${name}!</p>`
}));
Critical: {@render ...}
Render snippet:
{@render snippetName(args)}
{@render children?.()} <!-- 可选链安全调用 -->
{@render (cond ? a : b)()} <!-- 任意表达式 -->
Critical: {@html ...}
Insert raw HTML (beware XSS risk):
{@html rawHtml}
{expression}automatically escapes HTML entities{@html expression}directly inserts raw HTML; ensure content is trusted- Must becomplete and self-contained HTML (cannot span
{@html}to concatenate tags) - Not affected by scoped styles — use
:globalto wrap
Critical: {@attach ...}
Functions that run when an element is mounted (Svelte 5.29+), replacing use: action:
<script>
/** @type {import('svelte/attachments').Attachment} */
function tooltip(node) {
const tip = createTip(node);
return { destroy() { tip.destroy(); } };
}
</script>
<button {@attach tooltip()}>hover</button>
Attachment Factory with parameters
function tooltip(content) {
return (node) => {
const tip = createTip(node, { content });
return { destroy() { tip.destroy(); } };
};
}
<button {@attach tooltip(content)}>hover</button>
Conditional Attachment
<div {@attach enabled && myAttachment}>...</div>
Inline Attachment
<canvas {@attach (canvas) => {
const ctx = canvas.getContext('2d');
$effect(() => { ctx.fillStyle = color; /* ... */ });
}}></canvas>
Re-run behavior
{@attach foo(bar)} in foo or bar changesfully rebuilds(unlike actions).
Converting from Action
import { fromAction } from 'svelte/attachments';
const attach = fromAction(myAction);
Critical: bind: directive
Data flows back from child to parent:
<!-- 简写 -->
<input bind:value />
<input bind:value={value} />
Function Bindings(5.9+)
<input bind:value={
() => value,
(v) => value = v.toLowerCase()
} />
Complete bind: list
| Directive | Element | Type |
|---|---|---|
bind:value |
input/textarea/select | Two-way |
bind:checked |
input[type=checkbox] | Two-way |
bind:indeterminate |
input[type=checkbox] | Two-way |
bind:group |
radio/checkbox group | Two-way |
bind:files |
input[type=file] | two-way |
bind:open |
details | two-way |
bind:value |
select multiple | array |
bind:currentTime/paused/volume/muted/playbackRate |
audio/video | two-way |
bind:duration/buffered/seeking/ended/readyState/played |
audio/video | read-only |
bind:videoWidth/Height |
video | read-only |
bind:naturalWidth/Height |
img | read-only |
bind:innerHTML/innerText/textContent |
contenteditable | two-way |
bind:clientWidth/Height/offsetWidth/Height/contentRect |
block-level element | read-only |
bind:this |
element/component | reference |
bind:innerWidth/Height |
svelte:window | two-way |
bind:scrollX/Y |
svelte:window | two-way |
bind:online |
svelte:window | two-way |
bind:fullscreenElement |
svelte:document | two-way |
bind:visibilityState |
svelte:document | two-way |
$bindable Props
<!-- 子组件 -->
<script>
let { value = $bindable() } = $props();
</script>
<input bind:value />
<!-- 父组件 -->
<Child bind:value={text} />
Critical: use: Action (primarily promoted before Svelte 5.29, recommended {@attach} for 5.29+)
<script>
/** @type {import('svelte/action').Action<HTMLElement, {color: string}>} */
function paint(node, data) {
node.style.color = data.color;
}
</script>
<div use: paint={{ color: 'red' }}>red text</div>
Action is only called once (no SSR); when parameters changedoes notre-run.
Critical: transition: / in: / out:
Transition when an element enters/leaves the DOM:
<script>
import { fade, fly } from 'svelte/transition';
let visible = $state(false);
</script>
{#if visible}
<div transition:fade>fades in and out</div>
<div in:fly={{ y: 200 }} out:fade>flies in, fades out</div>
{/if}
transition:two-way (can reverse mid-way)in:/out:one-way (parallel without interruption)- defaultlocal(
|globalmodifier responds to ancestor block)
built-in transitions
fade / blur / fly / slide / scale / draw / crossfade(from svelte/transition)
custom transition
function whoosh(node, params) {
return {
duration: 400,
easing: elasticOut,
css: (t, u) => `transform: scale(${t})`
};
}
transition events
onintrostart / onintroend / onoutrostart / onoutroend
Critical: animate:
Animation when existing elements within a keyed each blockreorder:
<script>
import { flip } from 'svelte/animate';
</script>
{#each list as item, i (item)}
<li animate:flip={{ duration: 300 }}>{item}</li>
{/each}
- does nottrigger on add/remove (use transition:)
- must keyed
{#each}direct child element flipis built-in (First-Last-In-First-Out smooth movement)
Critical: style: / class: / class
style: directive
<div style:color="red" style:width="12rem" style:--columns={n}>
<!-- 简写、表达式、!important、CSS 自定义属性都支持 -->
</div>
style: directive priorityhigher than style attribute (even with !important)。
class attribute (5.16+ supports objects/arrays)
<!-- 对象:truthy key 添加 -->
<div class={{ cool, lame: !cool }}>...</div>
<!-- 数组:falsy 过滤后合并 -->
<div class={[faded && 'saturate-0', large && 'scale-200']}>...</div>
<!-- 嵌套扁平化 -->
<div class={['btn', { primary: true }, ['rounded', { large }]]}>...</div>
ClassValue type
<script lang="ts">
import type { ClassValue } from 'svelte/elements';
const props: { class: ClassValue } = $props();
</script>
class: directive (traditional, no longer recommended in 5.16+)
<div class:cool={cool} class:lame={!cool}>
<!-- 简写 class:cool -->
</div>
Critical: {@const ...}
⚠️ Legacy syntax, recommend using
{const x = $derived(y)}instead
Define constants within block scope:
{#each boxes as box}
{@const area = box.width * box.height}
<p>{box.width} × {box.height} = {area}</p>
{/each}
Can only be {#if}、{#each}、{#snippet}、<Component />、<svelte:boundary> direct child element.
Critical: {let/const ...} (recommended in 5.56+)
{#each boxes as box}
{const area = box.width * box.height}
<p>{area}</p>
{/each}
Can be anywhere in the component; references external values, scope is lexical siblings/children.
Reactive:
{let name = $state(user.name)}
{const greeting = $derived(`Hello ${name}`)}
Critical: {@debug ...}
Debug label, prints to console when variable changes:
{@debug user, count} <!-- 指定变量 -->
{@debug} <!-- 任意状态变化断点 -->
Note:{@debug} can only accept variable names (not expressions).
Quick Fixes
| Problem | Solution |
|---|---|
| List does not update | Use keyed {#each items as item (item.id)} |
| Empty list has no fallback | Add {:else} branch |
| Transition does not trigger | Use {#key value} wrapping to trigger rebuild |
{@html} style does not take effect |
Use :global or do not use {@html} |
| Snippet is not visible | Confirm it is within the same lexical scope |
await sync behavior does not work |
Enable experimental.async |
| Reverse transition does not work | Use transition: instead of in: / out: |
animate: does not trigger |
Must be keyed {#each} + direct child element |
Gotchas
{#each}key must be a unique value — using the index (i) as the key will cause incorrect efficient updates{@html}is not affected by scoped styles — inserted HTML does not include Svelte scoped hash{@debug}cannot debug expression — only variable names are accepted- Snippet is visible within lexical scope — nesting level determines visibility
{@attach}is Svelte 5.29+ — older versions useuse:action- Event attributes start with
onstart with —onclickis noton:click(Svelte 4 syntax is deprecated) {@attach}rerun full setup — opposite of action not rerunning; for expensive setup usegetBarform wrapperclassproperty'sfalse/NaNserialized as string — 5.16+ use object formbind:groupis valid only within the same component — invalid across components- Transitions do not run during SSR — activated after client hydration
FAQ
Q: {#each} in key what is the purpose of
A: Helps Svelte diff efficiently: when data changes, precisely identify which element changed by key (insert/move/delete), avoiding full re-rendering.
Q: What is the difference between Snippet and Slot? A: Snippet is a new mechanism in Svelte 5, more powerful and flexible. Slot can only pass markup; Snippet can receive parameters, can be used directly at the definition site, can be passed to components, and shares lexical scope.
Q: {@html} and ordinary text interpolation?
A: {expr} automatically escapes HTML;{@html expr} inserts raw HTML directly. Raw HTML is not affected by scoped styles.
Q: When to use {#key}?
A: When an animation/rebuild needs to be triggered by an expression change, such as route switching, user switching, etc.
Q: use: and {@attach} how to choose?
A: Svelte 5.29+ recommends {@attach} — more flexible (reactive parameters, usable with components). When the old library only provides action, use fromAction(act) wrapper.
Q: transition: vs in: / out:?
A: transition: bidirectional reversible;in: / out: one-way parallel playback does not interrupt each other.
Q: animate: and transition: difference?
A: animate: is existing elements in keyed eachreorderingwhen;transition: is when elements enter/leave the DOM.
Q: When to use await expression vs {#await} block?
A: New code recommends await expression (requires experimental.async), supports synchronous updates, concurrent. Old code or simple branching can use {#await} block.
Examples
For runnable code examples, see examples/ directory:
| File | Content |
|---|---|
basic-markup.md |
Tags, attributes, spread, text expressions, comments, event delegation |
if-await-snippet.md |
{#if}/{#each}/{#await}/{#snippet} from basics to recursion |
snippet-advanced.md |
Snippet scope, explicit/implicit prop, optional, typed, module export, createRawSnippet |
render-tags.md |
{@render}/{@html}/{@debug}/{@const}/{@attach} |
attachments.md |
{@attach} factory / inline / conditional / convert action |
use-action.md |
use: directive + Action type signature + use→{@attach} migration |
transitions.md |
transition: / in: / out: parameters, custom functions, events, crossfade |
animations.md |
animate:flip / custom function / combined with transition |
await-expressions.md |
synchronized / concurrent / pending / fork / SSR |
style-class.md |
style: directives, class object/array, class: legacy, ClassValue |
bind-directives.md |
bind:value/checked/group/files/this/offsetWidth/Function bindings |
event-handlers.md |
onclick, event bubbling, delegation, window/document events |
References
For an in-depth technical reference, see references/ Table of contents:
| File | Content |
|---|---|
basic-markup.md |
Tag semantics, attribute rules, event delegation, comments, SSR behavior |
if-await-each.md |
Complete syntax of {#if}/{#else}/{#each}/{#await} and the keyed mechanism |
snippet-render.md |
Snippet definition/call/recursion/argument passing, @render/@const type signatures |
snippet-advanced.md |
Explicit/implicit props, optional snippets, createRawSnippet, module exports, Snippet vs Slot |
special-tags.md |
{@html}/{@debug}/{@attach}/svelte:element |
attachments.md |
{@attach} semantics, reactive re-runs, fromAction migration, svelte/attachments API |
transitions.md |
Local/global, built-in transitions, custom function css/tick, events, crossfade |
animations.md |
animate:flip, custom functions (from/to DOMRect), combining with transition |
await-expressions.md |
Synchronous updates, concurrency, $effect.pending(), settled(), fork, SSR |
style-class.md |
style: precedence, class object/array/ClassValue, !important rules |
bind-reference.md |
Complete list of all bind: directives and the elements they apply to |
event-reference.md |
Event handling: onclick vs on:click, window/document events |