Loading...
Loading...
shadcn-svelte (bits-ui) component integration for Inertia Rails Svelte (NOT SvelteKit): forms, dialogs, tables, toasts, dark mode, and more. Use when building UI with shadcn-svelte components in an Inertia + Svelte app or adapting shadcn-svelte examples from SvelteKit. Wire shadcn-svelte inputs to Inertia Form via name attribute and {#snippet} syntax. Flash toasts require Rails flash_keys initializer config.
npx skill4agent add inertia-rails/skills shadcn-svelte-inertiagoto$app/navigationload+page.svelteroutersveltekit-superformszod<Form>name| shadcn-svelte default (SvelteKit) | Inertia equivalent |
|---|---|
| |
| Server-rendered props via Rails controller |
| Default exports with module script layout |
| Inertia |
| |
npx shadcn-svelte@latest init@/tsconfig.json@/vite.config.tsvite-plugin-ruby<Form>InputLabelButtonname<Form>inertia-rails-formsreferences/svelte.md<Form>{#snippet}<script lang="ts">
import { Form } from '@inertiajs/svelte'
import { Input } from '$lib/components/ui/input'
import { Label } from '$lib/components/ui/label'
import { Button } from '$lib/components/ui/button'
</script>
<Form method="post" action="/users">
{#snippet children({ errors, processing })}
<div class="space-y-4">
<div>
<Label for="name">Name</Label>
<Input id="name" name="name" />
{#if errors.name}<p class="text-sm text-destructive">{errors.name}</p>{/if}
</div>
<div>
<Label for="email">Email</Label>
<Input id="email" name="email" type="email" />
{#if errors.email}<p class="text-sm text-destructive">{errors.email}</p>{/if}
</div>
<Button type="submit" disabled={processing}>
{processing ? 'Creating...' : 'Create User'}
</Button>
</div>
{/snippet}
</Form><Form let:errors let:processing>{#snippet}<Select>name<Form><Select name="role" value="member">
<SelectTrigger><SelectValue placeholder="Select role" /></SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="member">Member</SelectItem>
</SelectContent>
</Select><script lang="ts">
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '$lib/components/ui/dialog'
import { router } from '@inertiajs/svelte'
let { open, user }: { open: boolean; user: User } = $props()
</script>
<Dialog
{open}
onOpenChange={(isOpen) => { if (!isOpen) router.replaceProp('show_dialog', false) }}
>
<DialogContent>
<DialogHeader>
<DialogTitle>{user.name}</DialogTitle>
</DialogHeader>
<!-- content -->
</DialogContent>
</Dialog>on:openChangeonOpenChange<script lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '$lib/components/ui/table'
import { router } from '@inertiajs/svelte'
let { users, sort }: { users: User[]; sort: string } = $props()
const handleSort = (column: string) => {
router.get('/users', { sort: column }, { preserveState: true })
}
</script>
<Table>
<TableHeader>
<TableRow>
<TableHead class="cursor-pointer" onclick={() => handleSort('name')}>
Name {sort === 'name' ? '↑' : ''}
</TableHead>
<TableHead>Email</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{#each users as user (user.id)}
<TableRow>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
</TableRow>
{/each}
</TableBody>
</Table><Link>use:inertia<a>flash_keysinertia-rails-controllers$page.flashinertia-rails-pagesreferences/flash-toast.mdnpx shadcn-svelte@latest init@custom-variant dark (&:is(.dark *));<head><%# app/views/layouts/application.html.erb — in <head>, before any stylesheets %>
<script>
document.documentElement.classList.toggle(
"dark",
localStorage.appearance === "dark" ||
(!("appearance" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches),
);
</script>useAppearancematchMedia.dark<html><svelte:head><Head><svelte:head><Head><svelte:head>
<title>{user.name} - Profile</title>
</svelte:head>bind:value<Form><Form>namebind:value<Form><!-- BAD — bind:value is ignored by <Form> on submit -->
<Form method="post" action="/users">
<Input bind:value={name} />
</Form>
<!-- GOOD — name attribute is what <Form> reads -->
<Form method="post" action="/users">
<Input name="name" />
</Form>bind:valueuseForm$form.name$page<script lang="ts">
import { page } from '@inertiajs/svelte'
// BAD — snapshot, won't update after navigation:
// let user = $page.props.auth.user
// GOOD — use $derived for reactive access:
let user = $derived($page.props.auth.user)
</script>$: user = $page.props.auth.useruse:inertia<Link><Link><script lang="ts">
import { inertia } from '@inertiajs/svelte'
</script>
<tr use:inertia={{ href: `/users/${user.id}` }} class="cursor-pointer">
<td>{user.name}</td>
</tr>transition*forceMount| Symptom | Cause | Fix |
|---|---|---|
| Form components crash | Using shadcn-svelte form components that depend on superforms | Replace with plain |
| Missing | Add |
| Dialog closes unexpectedly | Missing or wrong | Use |
| Flash of wrong theme (FOUC) | Missing inline | Add dark mode script before stylesheets |
| | Use |
| Shared props stale after navigation | Destructured | Use |
inertia-rails-formsreferences/svelte.md<Form>inertia-rails-controllersinertia-rails-pagesreferences/svelte.mdinertia-rails-pagesreferences/svelte.mduse:inertiainertia-rails-pagesreferences/svelte.mdreferences/components.mdcomponents.md