Type something to search...
How to use Svelte components in Astro?

How to use Svelte components in Astro?

To use Svelte components in Astro, run npx astro add svelte to install the official @astrojs/svelte integration, then import a .svelte file into any .astro page and use it as a tag. Astro renders it to HTML on the server and ships no JavaScript by default; add a client directive such as client:load or client:visible to hydrate it so runes like $state and event handlers work in the browser. The current integration targets Svelte 5, so props come from $props(), and content passed from Astro arrives as snippets you render with {@render children?.()}.

Svelte pairs naturally with Astro. Both compile away as much as possible, and Svelte's small runtime makes it one of the cheapest frameworks to hydrate. In this article you'll learn how to install and configure Svelte, write a component with runes, hydrate it, pass props, children and named slots, use Svelte stores, configure the compiler, and avoid the mistakes that come from server rendering.

Installing the Svelte Integration

Run the astro add command:

npx astro add svelte

It installs @astrojs/svelte and svelte, adds the integration to your Astro config, and creates a svelte.config.js file:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import svelte from '@astrojs/svelte';

export default defineConfig({
  integrations: [svelte()],
});
// svelte.config.js
import { vitePreprocess } from '@astrojs/svelte';

export default {
  preprocess: vitePreprocess(),
};

vitePreprocess() lets you write lang="ts" in script blocks and use preprocessors like Sass in style blocks. It also helps the Svelte editor extension understand your files.

The current @astrojs/svelte (9.1.1 at the time of writing) supports Svelte 5 only. If you have a project that still needs Svelte 3 or 4, the Astro docs say to install @astrojs/svelte@5 instead.

Writing a Svelte 5 Component

Here's a counter written with Svelte 5 runes:

<!-- src/components/SvelteCounter.svelte -->
<script lang="ts">
  interface Props {
    label: string;
    start?: number;
  }

  let { label, start = 0 }: Props = $props();
  let count = $state(start);
</script>

<button type="button" onclick={() => count++}>{label}: {count}</button>

A few Svelte 5 details to notice if you're coming from older tutorials:

  • $props() replaces export let for declaring props.
  • $state() marks reactive state. Plain let variables are no longer reactive by themselves.
  • onclick is a normal attribute. The on:click directive syntax is from Svelte 4.

Use it in an Astro page like any other component:

---
// src/pages/index.astro
import SvelteCounter from '../components/SvelteCounter.svelte';
---
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Svelte in Astro</title>
  </head>
  <body>
    <SvelteCounter label="Clicks" start={2} />
  </body>
</html>

Without a directive, this outputs a button reading "Clicks: 2" and nothing else. The component ran on the server, and clicking does nothing.

Hydrating the Component

Add a client directive to make it interactive:

<SvelteCounter label="Clicks" start={2} client:idle />

Here's the output from my test build, with long attributes shortened:

<astro-island ... component-url="/_astro/SvelteCounter.5qjqaW-w.js" ... props="{&quot;label&quot;:[0,&quot;Clicks&quot;],&quot;start&quot;:[0,2]}" ssr client="idle" ...><!--[--><button type="button">Clicks: 2</button><!--]--><!--astro:end--></astro-island>

The <!--[--> and <!--]--> comments are Svelte's hydration markers. Once the browser is idle, Astro loads the component and Svelte's client code and hydrates the existing button.

Svelte's runtime is small. In my test build, the shared Svelte client chunk was about 12 to 14 KB gzipped, compared with about 66 KB for React's renderer. If you're adding a framework mainly for a few interactive widgets, that difference adds up. Choosing between client:load, client:idle and client:visible is covered in what the client directives mean.

Passing Props

Props behave the same as in Svelte apps. During server rendering any value works; for hydrated components the props are serialised into the HTML, so they must be serialisable types: plain objects, strings, numbers, arrays, Map, Set, Date, RegExp, BigInt, URL, typed arrays and Infinity.

You can't pass functions, so callback props like onSave={...} written in an .astro file won't reach a hydrated component. Keep the callback inside Svelte, either in the component itself or in a parent Svelte component in the same island.

Children and Named Slots as Snippets

In Svelte 5, content passed between a component's tags is a snippet. Astro follows that model: the default content arrives as a children snippet, and every element with a slot attribute arrives as a snippet prop with that name.

<!-- src/components/SvelteCard.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte';

  interface Props {
    title?: Snippet;
    children?: Snippet;
  }

  let { title, children }: Props = $props();
</script>

<article class="card">
  <header>{@render title?.()}</header>
  <div class="card-body">{@render children?.()}</div>
</article>
---
// src/pages/index.astro
import SvelteCard from '../components/SvelteCard.svelte';
---
<SvelteCard client:load>
  <h2 slot="title">Menu</h2>
  <p>Default slot content.</p>
</SvelteCard>

The optional chaining in title?.() matters: if a page doesn't pass that slot, the snippet is undefined, and calling it would throw.

The hydrated output wraps each piece of slot content in an <astro-slot> element:

<article class="card"><header><astro-slot name="title"><h2>Menu</h2></astro-slot><!----></header> <div class="card-body"><astro-slot><p>Default slot content.</p></astro-slot><!----></div></article>

Two things to keep in mind:

  • Slot names aren't converted: unlike React, kebab-case names stay kebab-case. A slot="footer-links" arrives as a prop literally named footer-links, which you can't destructure as a normal identifier. Prefer single-word or camelCase slot names for Svelte components.
  • Legacy slots still work: the integration also passes $$slots, so older components that use <slot /> and <slot name="title" /> continue to render.

Slot content is rendered by Astro before Svelte sees it, which is also how you put Astro components inside a Svelte component: pass them in as children or named slots from an .astro file.

Using Svelte Stores

Svelte's $store syntax works with any object that follows the store contract, including Nano Stores. That makes it easy for a Svelte island to read state written by a React or Vue island on the same page:

// src/stores/cart.ts
import { atom, map } from 'nanostores';

export type CartItem = { id: string; name: string; quantity: number };

export const isCartOpen = atom(false);
export const cartItems = map<Record<string, CartItem>>({});
<!-- src/components/CartFlyout.svelte -->
<script lang="ts">
  import { cartItems, isCartOpen } from '../stores/cart';
</script>

{#if $isCartOpen}
  <aside>
    <h2>Your cart</h2>
    <ul>
      {#each Object.values($cartItems) as item (item.id)}
        <li>{item.name} x {item.quantity}</li>
      {:else}
        <li>Your cart is empty.</li>
      {/each}
    </ul>
  </aside>
{/if}

No helper package is needed for Svelte. I tested this flyout with client:load alongside a React "Add to cart" button and a Vue cart badge, and all three stayed in sync. The full example is in how to share state between islands.

Svelte's own writable() stores work just as well when all the islands sharing the state are Svelte components.

Configuring the Svelte Compiler

The integration is built on @sveltejs/vite-plugin-svelte, and options you pass to svelte() go to that plugin. You can set them in either astro.config.mjs or svelte.config.js; if both set the same option, the Astro config wins:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import svelte from '@astrojs/svelte';

export default defineConfig({
  integrations: [svelte({ extensions: ['.svelte'] })],
});

For most projects the defaults are fine, and the only config you'll touch is the preprocess entry in svelte.config.js.

Choosing a Directive for Svelte Islands

Because Svelte's runtime is small, it's tempting to hydrate everything with client:load. The directive still matters, though, because each component's own code and its data have to download and run, and that work competes with the rest of the page during start-up. A sensible mapping:

Svelte componentDirective
Header search or menu used straight awayclient:load
Theme toggle, copy buttons, small formsclient:idle
Comments, galleries, charts lower on the pageclient:visible
Mobile-only navigationclient:media="(max-width: 48em)"
Component that reads window while renderingclient:only="svelte"
Cards, lists and layout pieces with no interactionNo directive

The last row is easy to forget. Svelte components make perfectly good server templates, and rendering them without a directive costs nothing in the browser. If a component only displays props, leave it static and let Astro output plain HTML.

Server Rendering Gotchas

Browser APIs at the Top Level

The <script> block runs on the server during rendering. Reading window, document or localStorage at the top level fails there. Put browser-only code in $effect() or onMount(), which only run in the browser, or use client:only="svelte" to skip server rendering for that component. The trade-offs of the second option are covered in how to use client:only.

Forgetting the Directive

A Svelte component without a client directive is static HTML. Its $state never updates and its handlers are never attached. If a component looks right but doesn't respond, check for a missing client:* first.

Each Island Is Its Own Component Tree

Every hydrated Svelte component is mounted separately. Context set with setContext() in one island isn't visible from another. Use a store module for anything several islands share.


FAQ: Svelte Components in Astro

The current @astrojs/svelte integration supports Svelte 5. Many Svelte 4 components still compile in Svelte 5's compatibility mode, but for projects that must stay on Svelte 3 or 4, the docs recommend installing @astrojs/svelte version 5.

No. Astro handles routing, rendering and building. You only need the svelte package and the @astrojs/svelte integration.

It's most likely missing a client directive, so it was rendered as static HTML. Also check that the value is declared with $state in Svelte 5, because plain let variables are not reactive.

Destructure children and any named slots from $props(), then render each one with a Svelte render tag that calls the snippet, such as children or title. Each named slot arrives as a snippet prop with the same name as the slot attribute.

Yes. Put the state in a framework-agnostic store such as Nano Stores. Svelte reads it with the $ prefix, and React reads it with the useStore hook from @nanostores/react.

Yes. Styles in a Svelte component's style block are scoped by the Svelte compiler and bundled with the page, just as they are in a SvelteKit app.


Conclusion

Svelte works in Astro with one command and a small config file. Write components with Svelte 5 runes, import them into .astro pages, and they render to HTML on the server. Add a client directive to hydrate the ones that need interactivity, render content passed from Astro with {@render children?.()} and named snippets, and use Svelte stores or Nano Stores when islands need to share state.

Because Svelte's runtime is small, it's a sensible choice when you want a component framework for interactive widgets without paying much in JavaScript. Keep browser-only code in $effect() or onMount(), prefer simple slot names, and hydrate only what needs to respond to the user.

Tags :
Share :

Related Posts

What are Astro Actions and how do they work?

What are Astro Actions and how do they work?

Astro Actions are type-safe backend functions that you define once on the server and then call from the browser as if they were ordinary async fu

Dive Deeper
What are Astro adapters and which one should you choose?

What are Astro adapters and which one should you choose?

An Astro adapter is an integration that builds the server part of your project for a specific runtime, such as Node.js, Vercel Functions, Netlify

Dive Deeper
How to build API endpoints in Astro?

How to build API endpoints in Astro?

To build an API endpoint in Astro, create a .js or .ts file in src/pages/ and export a function named after an HTTP method, such as GET or

Dive Deeper