| name | composition-svelte |
| description | Use when composing a Svelte 5 feature from provider/wrapper components, content snippets, reactive state, or third-party DOM bridging (maplibre, leaflet, d3, tippy, popper). Covers overlay wrappers, snippet APIs, and state location. |
| user-invocable | true |
composition-svelte
Decompose a Svelte 5 feature by ROLE. Wrong role assignment is the root cause of every architectural fuckup. Before generating files, pin three decisions: composition shape, snippet API, state location. Per-file contracts fall out from those.
Rules from frontend:code-style, svelte-5:code-style-svelte, frontend:editing, svelte:svelte-code-writer, svelte:svelte-core-bestpractices apply throughout. Run mcp__svelte__svelte-autofixer after every .svelte edit.
When to use
- Designing a wrapper / Provider-style component (popup, menu, drawer, modal, tooltip, panel, slide-in, accordion)
- Autofixer flagged
bind:this
- About to type
$effect(() => { new ThirdPartyThing(...) })
- About to write
prop={appState.x} or prop={someStore.value}
- Naming a new file
Default.svelte / Feature.svelte / Wrapper.svelte
- Calling
setDOMContent / appendChild(svelteRenderedNode) / similar
- Deciding default vs named snippets
- Fixing a focus / ESC / keyboard bug in one of several similar menus
- About to add a maplibre
Marker, leaflet L.marker, d3 selection
- About to type
:global(...) inside a wrapper's <style> block
Skip for: CSS-only tweaks, test-only edits, backend / non-Svelte work.
Pin three decisions first
Answer in writing BEFORE generating files. Full tables and pointers in references/decisions.md.
- Shape: Composition shape / Non-Svelte Host?
- Snippet API: default
children, named snippets, or mix?
- State location: where each piece of state lives. Options can coexist; pick what fits each piece.
.svelte.ts modules (module-level $state exports, classes with $state fields), component-local $state, context (createContext), existing global state.
If intent is ambiguous, ask via AskUserQuestion, never infer silently.
Quick reference
| Need | Read |
|---|
| Picking shape and snippet/state design | references/decisions.md |
| Composition shape, seven-role contract + folder structure (covers both caller-content and view-dispatcher / Provider behaviour) | references/composition-shape.md |
| Non-Svelte Host shape | references/non-svelte-host.md |
| Lifecycle rune choice | references/lifecycle.md |
| Bridging to a third-party DOM library | references/third-party.md |
A11y for overlays (focus, ESC, :focus-visible) | references/a11y-overlays.md |
CSS scope (avoiding :global()) | references/css-scope.md |
| Counter-arguments for common rationalizations | references/rationalizations.md |
Naming
- Files by ROLE.
Default.svelte alone is too vague: DefaultItems.svelte reads. Feature.svelte / Wrapper.svelte are banned for new code.
- Function verbs:
subscribeX/unsubscribeX for event subscription; attachX/detachX for Svelte {@attach} lifecycle; mountX/unmountX for component lifecycle; createX/destroyX for factory pairs. Don't conflate.
- Variables per
frontend:code-style: element not el; event not e; result not res.
- Utils grouped by CATEGORY (
$utils/domActions.ts for all DOM attachments). NOT one file per export.
Util extraction
When a pattern appears in 2+ components, extract to $utils/<category>.ts. Don't pre-extract, first use is inline; second triggers the move.
Tooling
Delegate creating, editing, or reviewing any .svelte / .svelte.ts / .svelte.js file to the svelte:svelte-file-editor subagent (per agent:before-you-act "Consider before doing it yourself"), multi-file refactors most of all. The subagent runs in its own context window, so its docs lookup (mcp__svelte__get-documentation) and autofixer iteration (mcp__svelte__svelte-autofixer) don't spend the main agent's context.
Symmetric bugs
UX bugs in overlay-style components almost always span ALL overlay-style components. On fix: grep for analogous (role="menu", tabindex="-1", onkeydown.*Escape), confirm, apply symmetrically in the same change, test both.
Done close-path matrix
Overlay-specific. Other feature types (forms, tables, panels without close behaviour) need their own done-matrix, write one local to that feature, mirroring this structure (interactions and their expected post-state, exit paths, regression neighbours). Before declaring an overlay feature done, run ALL of these in a real browser via Playwright MCP:
Per agent:done: declaring done before this matrix is fraudulent. Per agent:asshole: don't dismiss failures as "not my problem".
Red flags: STOP and reconsider
| Red flag | Means | Fix |
|---|
prop={appState.x} / prop={someStore.value} | Prop-drilling a global | Import the store, read it directly |
bind:this with {@attach} autofixer suggestion | Wrong tool for element augmenters | references/lifecycle.md + references/third-party.md |
Default.svelte / Feature.svelte / Wrapper.svelte | Role not conveyed | Role-specific name |
| Third-party library import (maplibre / leaflet / d3 / etc.) inside the markup-only file mounted into a third-party container | Wrong axis | Move bridging to the parent Wrapper. See references/third-party.md. |
$effect(() => new ThirdPartyThing(...)) for one-shot setup | Wrong rune | onMount + cleanup return |
$effect writing to $state | Wrong rune | $derived |
| Fixing ESC for one menu only | Other menus have the same bug | Symmetric coverage |
| Two overlays visible simultaneously | Opened directly instead of through the Controller's handler | Funnel every open through the Controller's handler, it closes any prior overlay before opening the new one. See references/composition-shape.md Controller. |
| Auxiliary element (marker dot, badge, etc.) still visible after popup auto-close | Visibility gated on popup.isOpen(), but the third-party lib fires 'close' AFTER popup.remove() | Reconcile unconditionally on open; addTo/remove are idempotent |
| Snippet inlined in consumer feels too long | Time to extract | <script module> re-export, references/composition-shape.md |
attachX named but uses map.on() | Verb mismatch | subscribeX |
:global(button) in a wrapper's <style> | Wrong place to style children | references/css-scope.md |
el / e / tmp variable names | Code-style violation | Rename |
When tempted to rationalize, read references/rationalizations.md.