Skip to content

SvelteKit + shadcn-svelte prompt

Rules that make your coding agent write Svelte 5 runes and SvelteKit 2 as they work today, with shadcn-svelte components on Bits UI and Tailwind CSS v4 tokens.

What it makes your agent good at

Without it, agents write Svelte 4: export let, $: statements, on:click, slots, createEventDispatcher and $app/stores, or they load data in onMount and guard pages only in a layout. This prompt gives them runes, snippets and event attributes, typed load functions and validated form actions, auth checked where the data is read, and shadcn-svelte's child snippet instead of asChild.

  1. Runes, not Svelte 4$state, $derived and $props instead of exported lets and $: statements; $effect only for side effects, $bindable only when a parent binds.
  2. Snippets and event attributeschildren rendered with {@render}, callback props instead of dispatched events, onclick instead of on:click, {@attach} for DOM behavior.
  3. Loads in the right placeServer loads for secrets and the database, the fetch argument in universal loads, PageProps types and $app/state.
  4. Validated form actionsZod-checked actions that return fail() with field errors, use:enhance so forms work without JavaScript, redirect() without throw.
  5. Auth where the data is readhooks.server.ts sets locals.user and every load, action and endpoint checks it; secrets only from private $env modules.
  6. shadcn-svelte on Bits UIComponents added with the CLI, the child snippet instead of asChild, bind:open, tv() variants and mode-watcher for dark mode.

The SvelteKit + shadcn-svelte rules

153 lines, current to Svelte 5.57, SvelteKit 2, shadcn-svelte on Bits UI and Tailwind CSS 4.3. Last reviewed October 23, 2026. Plain Markdown, no front matter: each agent's is below.

Download

sveltekit-shadcn-svelte.md

# SvelteKit + shadcn-svelte

You are working in a SvelteKit 2 project with Svelte 5, TypeScript, shadcn-svelte components built on Bits UI, and Tailwind CSS v4. Write code for the versions in `package.json`, not the ones in your training data: Svelte 5 replaced most of Svelte 4's syntax with runes, snippets and event attributes, and the old syntax only survives in legacy mode.

## Before you write code

- Read `package.json` for `svelte`, `@sveltejs/kit`, `bits-ui` (or `@ark-ui/svelte`) and `tailwindcss`, and `svelte.config.js` for the adapter, aliases and any `experimental` flags.
- Read `components.json` for the aliases (usually `$lib/components/ui` and `$lib/utils`), and look in the UI folder before adding a component: it may exist, with local changes to keep.
- Use the generated types: `./$types` in every route file. Run `npx svelte-kit sync` (or the dev server) after adding routes so they exist.

## Svelte 5 runes

- State: `let count = $state(0)`. Objects and arrays become deeply reactive proxies; mutate them directly. Use `$state.raw` for large data you replace whole (API results, long lists).
- Derived values: `let total = $derived(items.length)`, or `$derived.by(() => { … })` for several lines. Never compute state inside `$effect`.
- `$effect` is for side effects outside Svelte (DOM APIs, timers, subscriptions, analytics); return a cleanup function. It never runs on the server, so it can't load page data.
- Props come from one `$props()` call, with defaults and the rest spread:

```svelte
<script lang="ts">
  import type { Snippet } from "svelte";
  import type { HTMLButtonAttributes } from "svelte/elements";

  interface Props extends HTMLButtonAttributes {
    value?: string;
    onvaluechange?: (value: string) => void;
    children?: Snippet;
  }

  let { value = $bindable(""), onvaluechange, children, ...rest }: Props = $props();
  const id = $props.id();
</script>

<button {...rest} {id} onclick={() => onvaluechange?.(value)}>
  {@render children?.()}
</button>
```

- Mark a prop `$bindable()` only when the parent needs `bind:value`; otherwise pass a callback prop. `$props.id()` gives ids that match between server and client.
- `$inspect(value)` is for debugging: remove it before finishing.
- Shared reactive state lives in `.svelte.ts` modules as an object or class with `$state` fields; you can't export a `let` that you reassign. On the server, module state is shared by every request: keep per-user data in `event.locals`, load data or context (`createContext` returns a typed getter and setter).
- Use `SvelteMap`, `SvelteSet`, `SvelteURL` and `MediaQuery` from `svelte/reactivity` instead of wrapping built-ins by hand.

## Templates

- Events are attributes: `onclick={save}`, `oninput`, `onsubmit`. There are no modifiers: call `event.preventDefault()` in the handler.
- Components raise events through callback props (`onvaluechange`), not dispatched events.
- Content is passed as snippets: `children` rendered with `{@render children?.()}`, named ones declared with `{#snippet row(item)}…{/snippet}` and typed `Snippet<[Item]>`. A component stored in a variable renders as `<Icon />` directly.
- Attach DOM behavior with `{@attach tooltip(text)}`: attachments re-run when their state changes and pass through spread props. Wrap an existing action with `fromAction` from `svelte/attachments` rather than writing a new `use:` action.
- `class` takes arrays and objects (`class={["card", selected && "ring-2", { "opacity-50": disabled }]}`); merge with a caller's class through `cn()`.
- Key every list: `{#each items as item (item.id)}`. Catch rendering errors in a region with `<svelte:boundary>` and a `failed` snippet.
- Intercept a binding with function bindings: `bind:value={() => value, (next) => (value = next.trim())}`.

## Routes and loading

- Files in `src/routes`: `+page.svelte`, `+layout.svelte`, `+page.ts` (universal load: runs on the server and in the browser), `+page.server.ts` (server only: database, secrets, cookies), `+server.ts` (endpoints), `+error.svelte`. Groups are `(app)`, params `[id]`, optional `[[lang]]`, rest `[...path]`.
- Pages read their props with the generated types; layouts render `children`:

```svelte
<script lang="ts">
  import type { PageProps } from "./$types";
  let { data, form }: PageProps = $props();
</script>
```

- Write loads as `export const load: PageServerLoad = async ({ params, locals, depends }) => { … }`. In universal loads use the `fetch` argument, never the global one: it forwards cookies and doesn't refetch on hydration. Never fetch your own `+server.ts` from a server load: call the function it would call.
- Return only what the page renders, never whole rows with private fields. To stream slow data, return its promise un-awaited as a property of a server load and render it with `{#await data.comments}`.
- `error(404, "Not found")` and `redirect(303, "/login")` from `@sveltejs/kit` throw by themselves: call them without `throw`, and outside any `try` that would swallow them.
- Refresh data with `depends("app:posts")` plus `invalidate("app:posts")`, or `invalidateAll()`, from `$app/navigation`.
- Read the current page from `$app/state`: `page.url`, `page.params`, `page.data`, `page.form`, `navigating`. It replaces `$app/stores`.
- Navigate with plain `<a href>`; use `goto` for programmatic navigation inside the app only. Set `prerender`, `ssr` and `csr` as exports of `+page.ts` or `+layout.ts`.

## Forms and endpoints

- Mutations are form actions in `+page.server.ts`:

```ts
import { fail, redirect } from "@sveltejs/kit";
import { z } from "zod";
import type { Actions } from "./$types";

const Post = z.object({ title: z.string().trim().min(1).max(200) });

export const actions = {
  create: async ({ request, locals }) => {
    if (!locals.user) redirect(303, "/login");
    const parsed = Post.safeParse(Object.fromEntries(await request.formData()));
    if (!parsed.success) {
      return fail(400, { errors: z.flattenError(parsed.error).fieldErrors });
    }
    await createPost(locals.user.id, parsed.data); // your data layer
  },
} satisfies Actions;
```

- Post to them with `<form method="POST" action="?/create" use:enhance>` (`enhance` from `$app/forms`), so the form works before JavaScript loads. After a successful action SvelteKit reruns the page's loads; customize with the `use:enhance` callback and call `update()`.
- Return errors with `fail(status, data)` and read them from `form`; never echo passwords back.
- `+server.ts` endpoints export `GET`, `POST` and the rest, typed `RequestHandler`, and return `json(…)` or a `Response`. Use them for webhooks and outside clients, not your own pages.
- Remote functions (`query`, `form`, `command` from `$app/server` in `*.remote.ts` files) are experimental: use them only when `svelte.config.js` enables them, and validate their arguments with a schema too.

## Server, auth and environment

- `src/hooks.server.ts` exports `handle`, which reads the session cookie, sets `event.locals.user` and calls `resolve(event)`. Type it in `App.Locals` in `src/app.d.ts`; compose several hooks with `sequence` from `@sveltejs/kit/hooks`; log in `handleError` and return a safe message.
- A check in `+layout.server.ts` doesn't protect the page's actions or endpoints, and page loads don't wait for it. Check `locals.user` in every load, action and endpoint that touches private data.
- Set cookies with an explicit path: `cookies.set("session", id, { path: "/", httpOnly: true, secure: true, sameSite: "lax" })`.
- Secrets come from `$env/static/private` (build time) or `$env/dynamic/private` (runtime), which only server modules can import. Browser values need the `PUBLIC_` prefix and `$env/static/public`. Keep server-only code in `$lib/server/` or `*.server.ts` files: SvelteKit refuses to bundle them for the browser.

## shadcn-svelte components

- Add components with the CLI, `npx shadcn-svelte@latest add dialog`, never from memory. The source lands in the UI folder for you to own and edit; import it from the folder index as the generated files do (`import * as Dialog from "$lib/components/ui/dialog/index.js"`).
- Variants are defined with `tv()` from `tailwind-variants` in the component file (`buttonVariants`); merge classes with `cn()` from `$lib/utils`.
- Bits UI has no `asChild`. To render your own element, use the `child` snippet and spread its props; to only look like a button, give the trigger `class={buttonVariants({ variant: "outline" })}`.

```svelte
<Dialog.Root bind:open>
  <Dialog.Trigger>
    {#snippet child({ props })}
      <Button {...props} variant="outline">Edit profile</Button>
    {/snippet}
  </Dialog.Trigger>
  <Dialog.Content>
    <Dialog.Title>Edit profile</Dialog.Title>
    <Dialog.Description>Changes save when you press Save.</Dialog.Description>
  </Dialog.Content>
</Dialog.Root>
```

- Control roots with `bind:open` and `bind:value`; get the element with `bind:ref`. Every dialog has a title, every input a label.
- Style states with the data attributes Bits UI sets (`data-[state=open]:`, `data-disabled:`).
- Dark mode: `<ModeWatcher />` from `mode-watcher` in the root `+layout.svelte`, which sets the `dark` class on `<html>`; switch with its `toggleMode()` or `setMode("dark")`.

## Tailwind CSS v4

- `@tailwindcss/vite` runs before SvelteKit: `plugins: [tailwindcss(), sveltekit()]`. The stylesheet (`@import "tailwindcss";`) is `src/app.css`, imported once in the root `+layout.svelte`. No `tailwind.config.js` and no PostCSS setup for Tailwind.
- Tokens are OKLCH variables in `:root` and `.dark`, mapped in `@theme inline { --color-background: var(--background); }`, with `@custom-variant dark (&:is(.dark *));`. Use `bg-background` and `text-muted-foreground`, never raw palette colors or `hsl(var(--x))`.
- A `<style>` block that uses `@apply` needs `@reference "../app.css";` (the relative path to your stylesheet). Prefer utilities in markup.

## If the project uses Ark UI

When `package.json` has `@ark-ui/svelte` instead of `bits-ui`, import parts per component (`import { Dialog } from "@ark-ui/svelte/dialog"`), compose `Dialog.Root` and its parts, use `bind:open`, and render your own element through the `asChild` snippet, which receives a props function: `{#snippet asChild(props)}<button {...props()}>Open</button>{/snippet}`. Never mix Ark and Bits UI parts in one component.

## Do not

- Do not write `export let`, `$:`, `on:click`, `|preventDefault`, `<slot>`, `$$props`, `$$restProps`, `createEventDispatcher`, `beforeUpdate`, `afterUpdate` or `<svelte:component>`, and don't use stores for component state.
- Do not import `$app/stores` or read `$page`; do not put secrets in `+page.ts` or a `PUBLIC_` variable.
- Do not load data in `onMount` or `$effect` when a load function can, or derive state in an effect.
- Do not use `asChild` with Bits UI, add `tailwind.config.js`, or write `hsl(var(--x))` tokens.

## Before you finish

- Run the project's check script (`svelte-kit sync && svelte-check`) and the linter: zero errors and no accessibility warnings.
- Submit every form you touched with JavaScript disabled, then with it on.
- Load each page with a hard refresh: data is in the server HTML and the console shows no hydration or state mutation warnings.
- Call actions and endpoints without a session: expect a redirect or 401, never private data. Check light and dark mode.

11,382 characters, 1,580 words. Under Windsurf's 12,000-character limit for a rule, as every prompt here is.

Use it in your agent

The same rules, where your agent reads them. Each file has the front matter its agent needs, set to apply on every request.

Claude Code

A rule it loads every session

  1. Save it as .claude/rules/sveltekit-shadcn-svelte.md in your project, front matter included, or run the install command.
  2. Claude Code loads every file in .claude/rules with your CLAUDE.md when a session starts.
  3. Prefer one file? Paste it into CLAUDE.md under its own heading instead.

.claude/rules/sveltekit-shadcn-svelte.md

# SvelteKit + shadcn-svelteYou are working in a SvelteKit 2 project with Svelte 5, TypeScript, shadcn-svelte components built on Bits UI, and Tailwind CSS v4. Write code for the versions in `package.json`, not the ones in your training data: Svelte 5 replaced most of Svelte 4's syntax with runes, snippets and event attributes, and the old syntax only survives in legacy mode.## Before you write code- Read `package.json` for `svelte`, `@sveltejs/kit`, `bits-ui` (or `@ark-ui/svelte`) and `tailwindcss`, and `svelte.config.js` for the adapter, aliases and any `experimental` flags.- Read `components.json` for the aliases (usually `$lib/components/ui` and `$lib/utils`), and look in the UI folder before adding a component: it may exist, with local changes to keep.- Use the generated types: `./$types` in every route file. Run `npx svelte-kit sync` (or the dev server) after adding routes so they exist.## Svelte 5 runes…

Or let the shadcn CLI write this file: sign in (free) for your command.

Cursor

A project rule with front matter

  1. Save it as .cursor/rules/sveltekit-shadcn-svelte.mdc, front matter included, or run the install command.
  2. alwaysApply: true adds it to every Agent request in this project.
  3. Check it under Cursor Settings, Rules, then commit the folder for your team.

.cursor/rules/sveltekit-shadcn-svelte.mdc

---description: "Svelte 5 and SvelteKit 2 conventions with shadcn-svelte and Tailwind CSS v4: runes, snippets, load functions, form actions, hooks and components."alwaysApply: true---# SvelteKit + shadcn-svelteYou are working in a SvelteKit 2 project with Svelte 5, TypeScript, shadcn-svelte components built on Bits UI, and Tailwind CSS v4. Write code for the versions in `package.json`, not the ones in your training data: Svelte 5 replaced most of Svelte 4's syntax with runes, snippets and event attributes, and the old syntax only survives in legacy mode.## Before you write code- Read `package.json` for `svelte`, `@sveltejs/kit`, `bits-ui` (or `@ark-ui/svelte`) and `tailwindcss`, and `svelte.config.js` for the adapter, aliases and any `experimental` flags.- Read `components.json` for the aliases (usually `$lib/components/ui` and `$lib/utils`), and look in the UI folder before adding a component: it may exist, with local changes to keep.- Use the generated types: `./$types` in every route file. Run `npx svelte-kit sync` (or the dev server) after adding routes so they exist.## Svelte 5 runes…

Or let the shadcn CLI write this file: sign in (free) for your command.

Windsurf

A workspace rule for Cascade

  1. Save it as .windsurf/rules/sveltekit-shadcn-svelte.md, or run the install command.
  2. trigger: always_on applies it to every Cascade request in this workspace.
  3. Windsurf reads up to 12,000 characters per rule; this one is 11,382.

.windsurf/rules/sveltekit-shadcn-svelte.md

---trigger: always_on---# SvelteKit + shadcn-svelteYou are working in a SvelteKit 2 project with Svelte 5, TypeScript, shadcn-svelte components built on Bits UI, and Tailwind CSS v4. Write code for the versions in `package.json`, not the ones in your training data: Svelte 5 replaced most of Svelte 4's syntax with runes, snippets and event attributes, and the old syntax only survives in legacy mode.## Before you write code- Read `package.json` for `svelte`, `@sveltejs/kit`, `bits-ui` (or `@ark-ui/svelte`) and `tailwindcss`, and `svelte.config.js` for the adapter, aliases and any `experimental` flags.- Read `components.json` for the aliases (usually `$lib/components/ui` and `$lib/utils`), and look in the UI folder before adding a component: it may exist, with local changes to keep.- Use the generated types: `./$types` in every route file. Run `npx svelte-kit sync` (or the dev server) after adding routes so they exist.## Svelte 5 runes…

Or let the shadcn CLI write this file: sign in (free) for your command.

GitHub Copilot

Instructions for the repository

  1. Save it as .github/instructions/sveltekit-shadcn-svelte.instructions.md, or run the install command.
  2. applyTo: "**" applies it to every request in this repository.
  3. Or paste it into .github/copilot-instructions.md, which Copilot reads for every request.

.github/instructions/sveltekit-shadcn-svelte.instructions.md

---description: "Svelte 5 and SvelteKit 2 conventions with shadcn-svelte and Tailwind CSS v4: runes, snippets, load functions, form actions, hooks and components."applyTo: "**"---# SvelteKit + shadcn-svelteYou are working in a SvelteKit 2 project with Svelte 5, TypeScript, shadcn-svelte components built on Bits UI, and Tailwind CSS v4. Write code for the versions in `package.json`, not the ones in your training data: Svelte 5 replaced most of Svelte 4's syntax with runes, snippets and event attributes, and the old syntax only survives in legacy mode.## Before you write code- Read `package.json` for `svelte`, `@sveltejs/kit`, `bits-ui` (or `@ark-ui/svelte`) and `tailwindcss`, and `svelte.config.js` for the adapter, aliases and any `experimental` flags.- Read `components.json` for the aliases (usually `$lib/components/ui` and `$lib/utils`), and look in the UI folder before adding a component: it may exist, with local changes to keep.- Use the generated types: `./$types` in every route file. Run `npx svelte-kit sync` (or the dev server) after adding routes so they exist.## Svelte 5 runes…

Or let the shadcn CLI write this file: sign in (free) for your command.

Codex & AGENTS.md

The file most agents read

  1. Paste it into AGENTS.md at your project's root, under its own heading.
  2. Codex reads AGENTS.md before it starts work, and so do Cursor, Zed, Jules, Amp and Copilot's coding agent.
  3. A folder can have its own AGENTS.md: the one closest to the file being edited wins.

AGENTS.md

## SvelteKit + shadcn-svelteYou are working in a SvelteKit 2 project with Svelte 5, TypeScript, shadcn-svelte components built on Bits UI, and Tailwind CSS v4. Write code for the versions in `package.json`, not the ones in your training data: Svelte 5 replaced most of Svelte 4's syntax with runes, snippets and event attributes, and the old syntax only survives in legacy mode.### Before you write code- Read `package.json` for `svelte`, `@sveltejs/kit`, `bits-ui` (or `@ark-ui/svelte`) and `tailwindcss`, and `svelte.config.js` for the adapter, aliases and any `experimental` flags.- Read `components.json` for the aliases (usually `$lib/components/ui` and `$lib/utils`), and look in the UI folder before adding a component: it may exist, with local changes to keep.- Use the generated types: `./$types` in every route file. Run `npx svelte-kit sync` (or the dev server) after adding routes so they exist.### Svelte 5 runes…

AGENTS.md is shared with your own instructions, so paste the rules in: the copy sets their headings a level down.

Rules teach it. The MCP equips it. With Pro, your agent installs every component, block and icon it needs, from a prompt.

Get Pro

Pricing

Give your agent the parts, with Pro

The prompts are free. Every Pro plan adds the MCP server, so your agent installs the components, blocks and icons it writes code around.

4.9/5 out of 5 · 150+ reviewsfrom 150+ founders and developers.

  • Secure checkout via Stripe
  • Code lives in your repo
  • Keep what you've shipped if you cancel
  • License terms

SvelteKit + shadcn-svelte prompt FAQ

What the SvelteKit + shadcn-svelte prompt covers and how to use it in your agent.

How does shadcn-svelte replace asChild in the prompt?

Bits UI, which shadcn-svelte is built on, dropped asChild for a child snippet that receives the props to spread on your own element. The prompt shows that snippet on a dialog trigger, so the agent never nests a button inside another button.

Can I use the SvelteKit prompt with Ark UI for Svelte?

Yes. When package.json has @ark-ui/svelte, the agent imports parts per component, binds open on the root and renders its own element through the asChild snippet, which receives a props function to spread. It never mixes Ark and Bits UI parts in one component.

How does the SvelteKit + shadcn-svelte prompt handle dark mode?

It renders ModeWatcher from mode-watcher in the root layout, which sets the dark class on the html element, and switches with toggleMode or setMode. Tokens are OKLCH variables overridden under .dark and mapped with @theme inline.

4.9 out of 5 from 150+ developers

Rules teach it. The MCP equips it.

Every component, block and icon, installed by your AI agent on your stack. The prompts stay free.

Get Pro
  • Cancel anytime
  • Keep what you've shipped