Harness-engineering svelte-runes-pattern

Svelte Runes Pattern

install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/svelte-runes-pattern" ~/.claude/skills/intense-visions-harness-engineering-svelte-runes-pattern-b63e97 && rm -rf "$T"
manifest: agents/skills/claude-code/svelte-runes-pattern/SKILL.md
source content

Svelte Runes Pattern

Declare reactive state, derived values, and side effects in Svelte 5 using the runes API ($state, $derived, $effect, $props, $bindable)

When to Use

  • You are writing Svelte 5 components and need reactive local state
  • You are migrating a Svelte 4 component from
    let x
    declarations and
    $:
    reactive statements
  • You need to declare component props with type safety and optional defaults
  • You want to create two-way bindable props for form inputs or controlled components

Instructions

$state — reactive local state:

  1. Declare reactive variables with
    $state
    . Unlike Svelte 4's magic
    let
    ,
    $state
    is explicit and works anywhere (not just
    .svelte
    files):
<script lang="ts">
  let count = $state(0)
  let items = $state<string[]>([])
  let user = $state<User | null>(null)
</script>

<button onclick={() => count++}>{count}</button>
  1. Objects and arrays declared with
    $state
    are deeply reactive — mutations are tracked automatically:
<script lang="ts">
  let todos = $state([
    { id: 1, text: 'Buy milk', done: false }
  ])

  function toggle(id: number) {
    const todo = todos.find(t => t.id === id)
    if (todo) todo.done = !todo.done  // mutation tracked
  }
</script>
  1. Use
    $state.raw
    for large non-reactive objects that should only update on full reassignment:
let data = $state.raw<HeavyObject>(initialData);
// later:
data = newData; // triggers update; internal mutations do not

$derived — computed values:

  1. Replace
    $: computed = expr
    with
    $derived
    :
<script lang="ts">
  let count = $state(0)
  let doubled = $derived(count * 2)
  let isEven = $derived(count % 2 === 0)
</script>
  1. Use
    $derived.by
    for multi-line derived logic:
let filteredItems = $derived.by(() => {
  return items.filter((item) => item.active).sort((a, b) => a.name.localeCompare(b.name));
});

$effect — side effects:

  1. Replace
    $: { sideEffect() }
    and
    onMount
    with
    $effect
    . It runs after DOM updates, re-runs when dependencies change, and cleans up on re-run or destroy:
$effect(() => {
  const handler = (e: KeyboardEvent) => {
    if (e.key === 'Escape') closeModal();
  };
  window.addEventListener('keydown', handler);
  return () => window.removeEventListener('keydown', handler); // cleanup
});
  1. $effect
    does NOT run on the server. For SSR-compatible initialization, use
    onMount
    from
    svelte
    :
import { onMount } from 'svelte';
onMount(() => {
  initMap();
});

$props — component props:

  1. Declare props with
    $props()
    — it replaces
    export let
    declarations:
<script lang="ts">
  interface Props {
    name: string
    age?: number
    onSelect?: (value: string) => void
  }

  let { name, age = 18, onSelect }: Props = $props()
</script>
  1. Spread remaining props onto an element using rest destructuring:
<script lang="ts">
  let { class: className, ...rest }: { class?: string } & Record<string, unknown> = $props()
</script>
<div class={className} {...rest} />

$bindable — two-way bound props:

  1. Mark a prop as bindable to allow parent components to use
    bind:propName
    :
<!-- Child: InputField.svelte -->
<script lang="ts">
  let { value = $bindable('') }: { value?: string } = $props()
</script>
<input bind:value />

<!-- Parent: -->
<InputField bind:value={searchQuery} />

Details

Runes vs. Svelte 4 reactivity:

Svelte 4Svelte 5 Rune
let count = 0
let count = $state(0)
$: doubled = count * 2
let doubled = $derived(count * 2)
$: { effect() }
$effect(() => { effect() })
export let prop
let { prop } = $props()
export let val = $bindable
let { val = $bindable() } = $props()

Runes work outside .svelte files:

This is the key architectural difference from Svelte 4.

$state
,
$derived
, and
$effect
work in
.svelte.ts
and
.svelte.js
files — enabling reactive shared logic without stores:

// lib/useCounter.svelte.ts
export function useCounter(initial = 0) {
  let count = $state(initial);
  const doubled = $derived(count * 2);
  return {
    get count() {
      return count;
    },
    get doubled() {
      return doubled;
    },
    increment: () => count++,
  };
}

Getter pattern for exposing $state:

To expose reactive state from a function, return getters (not values) so the reactive signal flows through:

// Do this:
return {
  get count() {
    return count;
  },
};
// Not this (loses reactivity):
return { count };

$effect dependency tracking:

$effect
automatically tracks all
$state
and
$derived
values read during its execution. You do not declare dependencies manually. If you read a value inside a conditional branch, it only becomes a dependency when that branch is reached.

Source

https://svelte.dev/docs/svelte/what-are-runes

Process

  1. Read the instructions and examples in this document.
  2. Apply the patterns to your implementation, adapting to your specific context.
  3. Verify your implementation against the details and edge cases listed above.

Harness Integration

  • Type: knowledge — this skill is a reference document, not a procedural workflow.
  • No tools or state — consumed as context by other skills and agents.

Success Criteria

  • The patterns described in this document are applied correctly in the implementation.
  • Edge cases and anti-patterns listed in this document are avoided.