Loading...
Loading...
shadcn-vue component integration for Inertia Rails Vue 3 (NOT Nuxt): forms, dialogs, tables, toasts, dark mode, and more. Use when building UI with shadcn-vue components in an Inertia + Vue app or adapting shadcn-vue examples from Nuxt. Wire shadcn-vue inputs to Inertia Form via name attribute and #default scoped slot. Flash toasts require Rails flash_keys initializer config.
npx skill4agent add inertia-rails/skills shadcn-vue-inertiauseRouteruseFetch<NuxtLink>router<Link>vee-validatezod<Form>name| shadcn-vue default (Nuxt) | Inertia equivalent |
|---|---|
| Server-rendered props via controller |
| |
| |
| Inertia |
| Plain |
| |
FormFieldFormItemFormLabelFormMessageInputLabelSelectname<Form>errorsnpx shadcn-vue@latest init@/tsconfig.json@/vite.config.tsvite-plugin-ruby<Form>InputLabelButtonname<Form>inertia-rails-formsreferences/vue.md<Form>FormFieldFormItemFormMessage<script setup lang="ts">
import { Form } from '@inertiajs/vue3'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
</script>
<template>
<Form method="post" action="/users">
<template #default="{ errors, processing }">
<div class="space-y-4">
<div>
<Label for="name">Name</Label>
<Input id="name" name="name" />
<p v-if="errors.name" class="text-sm text-destructive">{{ errors.name }}</p>
</div>
<div>
<Label for="email">Email</Label>
<Input id="email" name="email" type="email" />
<p v-if="errors.email" class="text-sm text-destructive">{{ errors.email }}</p>
</div>
<Button type="submit" :disabled="processing">
{{ processing ? 'Creating...' : 'Create User' }}
</Button>
</div>
</template>
</Form>
</template><Select>name<Form><template>
<Select name="role" default-value="member">
<SelectTrigger><SelectValue placeholder="Select role" /></SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="member">Member</SelectItem>
</SelectContent>
</Select>
</template><script setup lang="ts">
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { router } from '@inertiajs/vue3'
defineProps<{ open: boolean; user: User }>()
</script>
<template>
<Dialog
:open="open"
@update:open="(isOpen) => { if (!isOpen) router.replaceProp('show_dialog', false) }"
>
<DialogContent>
<DialogHeader>
<DialogTitle>{{ user.name }}</DialogTitle>
</DialogHeader>
<!-- content -->
</DialogContent>
</Dialog>
</template><script setup lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { router } from '@inertiajs/vue3'
defineProps<{ users: User[]; sort: string }>()
const handleSort = (column: string) => {
router.get('/users', { sort: column }, { preserveState: true })
}
</script>
<template>
<Table>
<TableHeader>
<TableRow>
<TableHead class="cursor-pointer" @click="handleSort('name')">
Name {{ sort === 'name' ? '↑' : '' }}
</TableHead>
<TableHead>Email</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="user in users" :key="user.id">
<TableCell>{{ user.name }}</TableCell>
<TableCell>{{ user.email }}</TableCell>
</TableRow>
</TableBody>
</Table>
</template><Link><a>flash_keysinertia-rails-controllersusePage().flashinertia-rails-pagesreferences/flash-toast.mduseFlashnpx shadcn-vue@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>v-model<Form><Form>namev-model<Form><!-- BAD — v-model value is ignored by <Form> on submit -->
<Form method="post" action="/users">
<Input v-model="name" />
</Form>
<!-- GOOD — name attribute is what <Form> reads -->
<Form method="post" action="/users">
<Input name="name" />
</Form>v-modeluseFormform.nameusePage()computed()<script setup lang="ts">
import { usePage } from '@inertiajs/vue3'
import { computed } from 'vue'
const page = usePage()
// BAD — not reactive, won't update when page changes:
// const user = page.props.auth.user
// GOOD — reactive, updates on navigation:
const user = computed(() => page.props.auth.user)
</script>computed()@update:open@closeupdate:openclose@close<!-- BAD — @close is not emitted by shadcn-vue Dialog -->
<Dialog @close="handleClose">
<!-- GOOD — @update:open fires on open AND close -->
<Dialog :open="open" @update:open="(isOpen) => { if (!isOpen) handleClose() }">| Symptom | Cause | Fix |
|---|---|---|
| Using shadcn-vue form components that depend on vee-validate | 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 | Wrap derived values in |
inertia-rails-formsreferences/vue.md<Form>inertia-rails-controllersinertia-rails-pagesreferences/vue.mdinertia-rails-pagesreferences/vue.mdreferences/components.mdcomponents.md