---
title: "Write your islands in Vue"
audience: "anyone who would rather write Vue than React"
description: "You'll start a fresh app, point it at Vue and write a single-file component the server renders and the browser hydrates. Vue is the only framework in the…"
snippet_caption: "app/src/ui/Tonight.vue"
snippet: "<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport { useStore } from \"@snapfire/fsr/client/vue\";\n\nimport { plannedCount } from \"@src/store\";\n\nconst props = defineProps<{ count: number }>();\nconst held = useStore(plannedCount, props.count);\nconst open = ref(false);\n</script>\n\n<template>\n  <div class=\"tonight\">\n    <button class=\"tonight-count\" @click=\"open = !open\">{{ held.value }} for tonight</button>\n    <p v-if=\"open\" class=\"tonight-note\">Kept in the session. This is Vue state in the root layout, so it stays open as you move between recipes.</p>\n  </div>\n</template>\n\n<style scoped>\n.tonight-count {\n  border: 1px solid #1b1d21;\n  border-radius: 999px;\n  background: #fff;\n  padding: 6px 14px;\n  font: inherit;\n  cursor: pointer;\n}\n</style>"
---

import { Install } from "@src/ui/Install";

You'll start a fresh app, point it at Vue and write a single-file component the server renders and the browser hydrates. Vue is the only framework in the import map, the vendor tree, the bundle and the type declarations. The pages are lowered templates as usual and the Vue component is lowered too.

<Install captured="0.x" also={["snapfire_vue"]} />

## Install the Vue plugin

`snapfirec` doesn't compile Vue itself. When it sees a `.vue` file it goes looking for `snapfirec-vue` on your `PATH` and hands it every `.vue` file in the project in one batch. The plugin carries Vue's own compiler and runs it in QuickJS. That's the third `cargo install` in the box above:

```shell-session
$ snapfirec-vue --version
snapfirec-vue 0.2.1 (@vue/compiler-sfc 3.5.44)
```

That `@vue/compiler-sfc` version belongs to the plugin. It compiles your components whatever version of Vue you end up vendoring for the browser.

## Scaffold, then point it at Vue

A plain `fsr new` writes no framework, which is what you want here.

```shell-session
$ fsr new box
wrote     box/.gitignore
wrote     box/config/app.toml
wrote     box/config/development.toml
wrote     box/config/production.toml
wrote     box/app/importmap.json
wrote     box/app/src/main.ts
wrote     box/app/routes/layout.tsx
wrote     box/app/routes/page.loader.ts
wrote     box/app/routes/page.tsx
wrote     box/app/routes/not-found.tsx
wrote     box/app/routes/error.tsx
wrote     box/app/styles/app.css
wrote     box/app/generated/plan.sexp
wrote     box/app/generated/contracts/schemas.json
wrote     box/app/.fsr/bundle/routes/layout.tsx
wrote     box/app/.fsr/bundle/routes/error.tsx
wrote     box/app/.fsr/bundle/routes/not-found.tsx
wrote     box/app/.fsr/bundle/routes/page.tsx
wrote     box/app/generated/mdx.d.ts
wrote     box/app/generated/shell.json
wrote     box/app/generated/uploads.d.ts
wrote     box/app/generated/assets.d.ts
wrote     box/app/generated/assets.json
wrote     box/app/generated/assets.map.json
wrote     box/app/generated/native.d.ts
wrote     box/app/generated/services.d.ts
wrote     box/app/generated/elements.d.ts
wrote     box/app/generated/head.ts
wrote     box/app/generated/config.ts
wrote     box/app/generated/config.schema.json
wrote     box/app/generated/host.ts
wrote     box/app/generated/islands.ts
wrote     box/app/generated/client.ts
wrote     box/app/generated/testing.ts
wrote     box/app/tsconfig.json
wrote     box/app/tsconfig.build.json
types     @snapfire/fsr/authoring      fsr 0.21.0
types     @snapfire/fsr/client         fsr 0.21.0
types     @snapfire/fsr/desktop        fsr 0.21.0
recorded  typecheck.version = "7.0.2" in /path/to/box/config/app.toml
next      fsr use box/app react      # only if the application wants React; also vue, elements, htmx, tailwind, tera or mdx
next      fsr dev box/app
```

Run the `fsr use` line it suggests, with `vue` instead of `react`:

```shell-session
$ fsr use box/app vue
added     vue                          vue/vue.bundle.mjs  128682 bytes
types     @snapfire/fsr/authoring      fsr 0.21.0
types     @snapfire/fsr/client         fsr 0.21.0
types     @snapfire/fsr/desktop        fsr 0.21.0
types     vue                          vue 3.5.44
types     @vue/compiler-dom            @vue/compiler-dom 3.5.44
types     @vue/runtime-dom             @vue/runtime-dom 3.5.44
types     @vue/compiler-sfc            @vue/compiler-sfc 3.5.44
types     @vue/server-renderer         @vue/server-renderer 3.5.44
types     @vue/compiler-core           @vue/compiler-core 3.5.44
types     @vue/runtime-core            @vue/runtime-core 3.5.44
types     csstype                      csstype 3.2.3
types     @babel/parser                @babel/parser 7.29.11
types     magic-string                 magic-string 0.30.21
types     postcss                      postcss 8.5.31
types     @vue/shared                  @vue/shared 3.5.44
types     @vue/reactivity              @vue/reactivity 3.5.44
types     @babel/types                 @babel/types 7.29.11
types     source-map-js                source-map-js 1.2.2
wrote     box/app/generated/plan.sexp
wrote     box/app/generated/contracts/schemas.json
wrote     box/app/.fsr/bundle/routes/layout.tsx
wrote     box/app/.fsr/bundle/routes/error.tsx
wrote     box/app/.fsr/bundle/routes/not-found.tsx
wrote     box/app/.fsr/bundle/routes/page.tsx
wrote     box/app/generated/mdx.d.ts
wrote     box/app/generated/shell.json
wrote     box/app/generated/uploads.d.ts
wrote     box/app/generated/assets.d.ts
wrote     box/app/generated/assets.json
wrote     box/app/generated/assets.map.json
wrote     box/app/generated/native.d.ts
wrote     box/app/generated/services.d.ts
wrote     box/app/generated/elements.d.ts
wrote     box/app/generated/head.ts
wrote     box/app/generated/config.ts
wrote     box/app/generated/config.schema.json
wrote     box/app/generated/host.ts
wrote     box/app/generated/islands.ts
wrote     box/app/generated/client.ts
wrote     box/app/generated/testing.ts
wrote     box/app/tsconfig.json
wrote     box/app/tsconfig.build.json
```

That one command vendors Vue as a plain file and fetches the declarations. The import map names Vue and nothing else:

```json title="app/importmap.json"
{
  "imports": {
    "vue": "/static/js/vendor/vue/vue.bundle.mjs"
  }
}
```

The host adds the FSR entries when it serves the page, one for each client module the bundle imports, so the file only names what `fsr use` vendored. There's no React anywhere in it. `fsr use box/app vue --example` would write a starter component too, which this tutorial writes by hand instead.

> The `*.vue` module declaration your editor wants is not there yet. It's written once there's a `.vue` import in the project to write it for, so run `fsr types box/app` again after the next section rather than now.

## Write the layout

The scaffolded layout already takes `Link` from `@snapfire/fsr/components` and types its children as `Children` from `@snapfire/fsr/template`, because a bare application has no React to type them against. That's the dialect this whole tutorial stays in:

```tsx title="app/routes/layout.tsx" {1,3,5,12-14}
import { Island, type Children } from "@snapfire/fsr/template";
import { Link } from "@snapfire/fsr/components";

import Tonight from "@src/ui/Tonight.vue";

export default function BoxLayout({ children, planned }: { children: Children; planned: number }) {
  return (
    <div className="shell">
      <header className="bar">
        <Link href="/" className="brand">
          box
        </Link>
        <Island when="load">
          <Tonight count={planned} />
        </Island>
      </header>
      <main className="content">{children}</main>
    </div>
  );
}
```

`Children` is what you write here where a React layout writes `ReactNode`. Nothing else about the file changes.

The build reads `import Tonight from "@src/ui/Tonight.vue"` and asks `snapfirec-vue` to describe the file: the template as Vue's own parser reads it, the `<script setup>` block and how the template reads each name the script binds. It lowers the component from that, to the same render tree a TSX template lowers to, so the server writes the component's markup. It still has to go inside `Island`: Vue's root is the island's, so a component Vue mounts can only be an island. Put it anywhere else and the build fails, naming the tag.

`planned` is a prop like any other, so something has to load it. The layout's loader does that and seeds the store key in the same file:

```ts title="app/routes/layout.loader.ts"
import type { Ctx } from "@snapfire/fsr/host";

export async function load(_ctx: Ctx) {
  return { planned: 2 };
}

export const store = ({ data }: { data: { planned: number } }) => ({ "box/planned": data.planned });
```

## Write the component

`app/src/ui/Tonight.vue` is an ordinary single-file component:

```vue title="app/src/ui/Tonight.vue"
<script setup lang="ts">
import { ref } from "vue";
import { useStore } from "@snapfire/fsr/client/vue";

import { plannedCount } from "@src/store";

const props = defineProps<{ count: number }>();
const held = useStore(plannedCount, props.count);
const open = ref(false);
</script>

<template>
  <div class="tonight">
    <button class="tonight-count" @click="open = !open">{{ held.value }} for tonight</button>
    <p v-if="open" class="tonight-note">Kept in the session. This is Vue state in the root layout, so it stays open as you move between recipes.</p>
  </div>
</template>

<style scoped>
.tonight-count {
  border: 1px solid #1b1d21;
  border-radius: 999px;
  background: #fff;
  padding: 6px 14px;
  font: inherit;
  cursor: pointer;
}
</style>
```

The two reactive values in the template behave differently. `open` is a real `ref`, so Vue unwraps it and you write `open`. `held` is not: `useStore` gives you back a reactive object with a `.value` on it, so you write `held.value`. Write `{{ held }}` and you'll render the object instead of the count.

`@snapfire/fsr/client/vue` is the same keyed store a React island reads through its own hook. The key is declared once in plain TypeScript; the adapter is the only part that differs by framework:

```ts title="app/src/store.ts"
import { key } from "@snapfire/fsr/client/store";

export const plannedCount = key<number>("box/planned");
```

The count arrives twice, as a prop and as a store key. The prop is the server's value for this render: it is in the markup, it is in the props script beside the marker and the component has it before any store exists, so the first paint is right. The store key makes it shared and reactive afterwards. `useStore(plannedCount, props.count)` reads the key if something already seeded it and falls back to the prop if not, then follows the key from then on. That is how a second island on the page, in any framework, can move this number without either component knowing about the other.

Without the prop, the button shows the store's default until the seed is read. Without the key, the count never changes from what this render said.

## Fetch the declarations

Now that there's a `.vue` file to declare, run it:

```shell-session
$ fsr types box/app
types     @snapfire/fsr/authoring      fsr 0.21.0
types     @snapfire/fsr/client         fsr 0.21.0
types     @snapfire/fsr/desktop        fsr 0.21.0
kept      vue
kept      @vue/compiler-dom
kept      @vue/runtime-dom
kept      @vue/compiler-sfc
kept      @vue/server-renderer
kept      @vue/compiler-core
kept      @vue/runtime-core
kept      csstype
kept      @babel/parser
kept      magic-string
kept      postcss
kept      @vue/shared
kept      @vue/reactivity
kept      @babel/types
kept      source-map-js
wrote     types/foreign.d.ts
wrote     tsconfig.json
```

`vue` and the packages its declarations import say `kept` because `fsr use` already fetched them. Declarations already sitting in `app/types/` are left alone unless you pass `--refresh`; the two fsr packages come out of the binary and are rewritten every time. `types/foreign.d.ts` is the new part: the `*.vue` module declaration that needed a `.vue` file to exist first.

`types/foreign.d.ts` is the shim that makes `import Tonight from "@src/ui/Tonight.vue"` typecheck. It's a loose declaration that takes any props:

```ts title="app/types/foreign.d.ts"
// Generated by fsr. Do not edit.

declare module "*.vue" {
  const component: (props: { [prop: string]: unknown }) => any;
  export default component;
}
```

`Tonight.vue` declares its props with `defineProps<{ count: number }>()`, so the build also writes a declaration of its own. The editor finds that one first:

```ts title="app/generated/vue/src/ui/Tonight.d.vue.ts"
declare const component: (props: ({ count: number }) & { children?: unknown }) => any;
export default component;
```

Pass the wrong type at the placement, `<Tonight count="two">`. The build fails at that line:

```shell-session
$ fsr build box/app
typecheck: tsc 7.0.2 from cache, 1 error
routes/layout.tsx(14,20): error TS2322: Type 'string' is not assignable to type 'number'.
```

## Build it

```shell-session
$ fsr build box/app --verbose
   Plugin cache: 0 of 1 answered
   Plugin:   vue 0.2.1 (@vue/compiler-sfc 3.5.44), through the driver
   Compiling TS: "src/main.ts"
   Compiling TS: "src/store.ts"
   Compiling VUE: "src/ui/Tonight.vue"
   Compiling CSS: "src/ui/Tonight.vue"
   ...
   Externals: '@snapfire/fsr/client', '@snapfire/fsr/client/store', '@snapfire/fsr/client/vue', 'vue'
routes    /                      routes
layouts   /                      routes/layout.tsx#default
sources   layout                 lowered     routes/layout.loader.ts
          $root                  lowered     routes/page.loader.ts
rendered  routes/error.tsx#default           lowered     static
          routes/layout.tsx#default          lowered     static
          routes/not-found.tsx#default       lowered     static
          routes/page.tsx#default            lowered     static
          src/ui/Tonight.vue#default         lowered     vue
...
plan      generated/plan.sexp    3.1 KB written
...
wrote /.../box/app/types/foreign.d.ts
wrote /.../box/app/generated/vue/src/ui/Tonight.d.vue.ts
typecheck tsc 7.0.2 from cache, clean
```

The component compiled to two files. `dist/src/ui/Tonight.vue` is the module, JavaScript under the source's name. `dist/src/ui/Tonight.vue.css` is the stylesheet, which holds the `<style scoped>` block with its `data-v-` attribute. The browser loads the module from `/static/js/app/src/ui/Tonight.vue`.

Read the last `rendered` row: the `.vue` file is `lowered`, with `vue` where a TSX template says `static`. The build read the component through `snapfirec-vue` and lowered it into the plan, so the host renders it. `foreign` in that column would mean it couldn't; a later section covers that case.

Every route module says `static`. A template with no state and no handlers of its own has nothing for the browser to change, so the build doesn't compile, register or bundle it. That's why the app loads no framework for its pages.

Add a `useState` to `routes/page.tsx` to see what happens otherwise. The page stops being `static`. The build cuts the part with state out of the page as `routes/page.island0.tsx` and the registry wants to mount it. A `.tsx` module mounts through React, which this app doesn't have:

```shell-session
$ fsr build box/app
`routes/page.island0.tsx#default` mounts through `@snapfire/fsr/client/react`, but the import map does not name `react` or `react-dom/client`; `fsr use <app dir> react` writes it
```

The build checks every adapter its registry would import against the import map before it writes anything, so you get the module, the adapter and the missing specifiers rather than a bare specifier error from the bundler. It also names the command that would fix it. You don't want React here, so take the `useState` back out instead.

Here's the registry the build wrote:

```ts title="app/generated/islands.ts"
import { registerDeferredIsland } from "@snapfire/fsr/client";

const vue = () => import("@snapfire/fsr/client/vue").then((m) => ({ mount: m.vueMounter, patch: m.vuePatcher, unmount: m.vueUnmounter }));

export function registerIslands(): void {
  registerDeferredIsland("src/ui/Tonight.vue#default", { loader: () => import("../src/ui/Tonight.vue").then((m) => m.default), adapter: vue });
}
```

`patch` runs when the island is already mounted and the server sends new props for it: after an action, after a revalidation or after a navigation that keeps the region. `vueMounter` mounts the component inside a one-element wrapper that holds its props reactively; `vuePatcher` assigns the new props onto that object, so Vue re-renders in place instead of tearing the island down and losing whatever state it had.

`unmount` handles removal. When a navigation takes away the markup an island was mounted in, the client hands the island to its entry's unmounter, nested islands first, so Vue runs the component's cleanup and the app is torn down. A root left running in a detached element never cleans up its effects, so it keeps whatever they hold.

`adapter` imports `@snapfire/fsr/client/vue`, which imports `vue` itself. The browser calls it the first time a page holds a Vue island, so a page with none loads no Vue. The `loader` defers the component module, `Tonight.vue` under `dist/`, the same way.

This application has no React island anywhere, so the build writes no React adapter and React is not reachable from the entry module at all. An island's adapter comes from its file extension: `.vue` goes to Vue's, anything the lowerer reads goes to React's. An extension no framework claims is a build error.

## Read the server's output

```shell-session
$ fsr serve box/app
$ curl -s localhost:3000/
<link rel="stylesheet" href="/static/js/app/styles/app.css">
...
<link rel="stylesheet" data-sf-css="" href="/static/js/app/src/ui/Tonight.vue.css">
...
<header class="bar"><a href="/" class="brand" data-sf-link="exact" aria-current="page">box</a><sf-s data-sf-island data-sf-region="routes/layout.tsx#default|i0" data-sf-when="load"><sf-i id="sf-i0" data-sf-module="src/ui/Tonight.vue#default" data-sf-ssr><div class="tonight" data-v-8c917f69><button class="tonight-count" data-v-8c917f69>2 for tonight</button><!----></div></sf-i><script type="application/json" data-sf-props="sf-i0">{"count":{"$":"f","v":2.0},"$sv":{"box/planned":{"$":"f","v":2.0}},"$k":"routes/layout.tsx#default|i0"}</script></sf-s></header>
...
<script type="application/json" data-sf-store>[{"k":"routes/layout.tsx#default","p":["content"],"v":{"box/planned":{"$":"f","v":2.0}}}]</script>
```

The component is in the markup. The server wrote what Vue's own server renderer would write for the same component and props, so Vue hydrates over it rather than mounting fresh, exactly as a React island does. Some of the markup is Vue's own spelling:

- `data-v-8c917f69` is the scoped style's stamp, on every element the component rendered. It matches the `data-v-` attribute in `Tonight.vue.css`.
- `<!---->` is the anchor where the `v-if` rendered nothing. Vue leaves a comment node so it knows where to put the paragraph when `open` flips.
- `2 for tonight` is the count, in the markup before any script runs. `useStore` read the key the layout's loader seeded, on the server.

`data-sf-ssr` on the marker says the server rendered the component, so the browser hydrates it. `$sv` in the props is the store values the server's render read, so hydration starts from the same count.

The last line is the store seed, written once at the end of the document from the loader's `store` export and tagged with the layout that seeded it. That is where `box/planned` comes from: the client reads the seed during `boot()` and `useStore` finds the key already set.

`data-sf-region` is the placement's key. It's how the navigator works out that the island in the new markup is the same one that's already mounted, so it can hand it fresh props instead of remounting it. That's the key `patch` uses.

The component's stylesheet is linked after the app's own, so its rules land later in the cascade. The compiler lists the file under `styles` in `dist/.snapfire-build.json` and the host reads that at boot.

## Pass children

Markup the layout writes inside a Vue island reaches the component as its default slot. Put a `<slot />` where it belongs:

```vue title="app/src/ui/Tonight.vue" {4}
<template>
  <div class="tonight">
    <button class="tonight-count" @click="open = !open">{{ held.value }} for tonight</button>
    <p v-if="open" class="tonight-note"><slot /></p>
  </div>
</template>
```

and write the children in the layout:

```tsx title="app/routes/layout.tsx" {2-4}
<Island when="load">
  <Tonight count={planned}>
    Kept in the session. <a href="/tonight">See them</a>.
  </Tonight>
</Island>
```

Build and fetch the page again:

```html
<sf-i id="sf-i0" data-sf-module="src/ui/Tonight.vue#default" data-sf-ssr><div class="tonight" data-v-1a03db4f><button class="tonight-count" data-v-1a03db4f>2 for tonight</button><!----></div><template data-sf-children>Kept in the session. <a href="/tonight">See them</a>.</template></sf-i>
```

`<template data-sf-children>` after the markup is the island's children. The panel is closed, so this render placed no `<slot />` and there was nowhere to write them. An inert `<template>` carries them instead, which the parser never shows. The mounter reads it before Vue hydrates and takes it out of the document, so the slot has its content the moment the panel opens.

The children are the layout's markup, so the server renders them wherever this render put the `<slot />`. Open the panel on the server and they sit inside it, in an `<sf-s data-sf-children>` region Vue hydrates as an element it rendered. With it closed, as above, there is no slot to write them into; they ride along in the inert `<template data-sf-children>` and the mounter puts them in place before Vue hydrates.

Either way the browser doesn't render them again; it keeps what the server wrote, including inside a `v-if` that opens later. A React island gets the same region as its `children`.

## Components the build can't read

Not every component lowers. Add a `<Teleport>` to a class selector under the paragraph in `Tonight.vue`:

```vue title="app/src/ui/Tonight.vue" {5}
<template>
  <div class="tonight">
    <button class="tonight-count" @click="open = !open">{{ held.value }} for tonight</button>
    <p v-if="open" class="tonight-note"><slot /></p>
    <Teleport to=".notes"><p>{{ held.value }} planned</p></Teleport>
  </div>
</template>
```

Build again:

```shell-session
$ fsr build box/app
rendered  src/ui/Tonight.vue#default         foreign     src/ui/Tonight.vue:16:5
foreign   src/ui/Tonight.vue:16:5            `<Teleport>` to a target that is neither `body` nor `#` and an id; the server places content only at the end of the body or at a `<Place>`
          1 component mounts in the browser for it, written empty by the server
            src/ui/Tonight.vue#default
```

The row says `foreign` instead of `lowered` and gives the file, line and column. The build carries on and the page still works: the server writes the marker empty with its props and Vue mounts the component fresh. Only that component loses its server-rendered first paint.

```html
<sf-i id="sf-i0" data-sf-module="src/ui/Tonight.vue#default"><sf-s data-sf-children>Kept in the session. <a href="/tonight">See them</a>.</sf-s></sf-i>
```

The children are still there, because they're the layout's markup rather than the component's.

What lowers is the subset a server can evaluate. In `<script setup>`: `defineProps` with `withDefaults` around it, `ref`, `shallowRef`, `computed` of an arrow, `reactive`, `useStore`, a `const` bound to an expression the build reads and functions, which are the browser's. Lifecycle and watch calls are passed over, since the server never runs one. In the template: interpolation, `v-if` / `v-else-if` / `v-else`, `v-for` with an item and an index, a bound attribute, `:class` as a string, array or object, `:style`, `v-show`, `v-html`, `v-text` and a plain `<slot />`. `v-model`, `v-bind` of a whole object, named and scoped slots between `.vue` files, `provide` with `inject` and the Options API lower too. `<Teleport to="body">` writes its content at the end of the body.

Residue is a component placed in the template that is not a `.vue` file the script imports, `v-slot`, a `<Teleport>` to a target that is neither `body` nor `#` and an id, a `<Teleport defer>` and a `created` that does anything but call `this.$emit`. Take the `<Teleport>` back out and the component goes back to `lowered`.

> An integer a contract types as `bigint` arrives as an integer, so a component that multiplies it by a literal fails the render on the server where the browser never noticed. Convert it at the placement, `serves={Number(recipe.serves)}`, the same way a loader has to.

## Use it with fsr dev

`fsr dev` watches `.vue` files like anything else. With `--verbose` it prints what each rebuild did. Save `Tonight.vue` and you get:

```shell-session
$ fsr dev box/app --verbose
...
dev: changed app/src/ui/Tonight.vue
...
   Compiling VUE: "src/ui/Tonight.vue"
   Compiling CSS: "src/ui/Tonight.vue"
typecheck tsc 7.0.2 from cache, clean
```

and the browser picks up the new module. It's a refresh rather than Vue's own HMR, so the component starts from its props again.

## When the plugin is missing

Take `snapfirec-vue` off your `PATH` and build:

```shell-session
$ fsr build box/app
❌ `snapfirec-vue` is not on PATH; `cargo install snapfire_vue` puts it there
   needed by "src/ui/Tonight.vue"
plugins   `snapfirec-vue` is not on PATH, so no `.vue` component was read or compiled; `cargo install snapfire_vue` puts it there, and until it does the bundle stops on the module the registry imports
snapfirec failed:
...
```

You get the binary it went looking for, the command that installs it and every file that needed it. The `plugins` row says what follows from it: with nothing to compile the `.vue` file, the module the registry imports is never written, so the bundle stops. The build exits with an error here and serves nothing.

## Host configuration

An island placement carries a module id. The registry maps that id to an adapter. Nothing in the plan, the payload or the renderer reads that id, which is why putting Vue where React was took no host configuration at all.

Next up: [205. Write your islands in Svelte](https://snapfirers.com/fsr/learn/205-write-your-islands-in-svelte).
