From inertia-rails-skills
Adapts shadcn-vue UI components for Inertia Rails Vue 3 apps (not Nuxt), replacing Nuxt patterns with Inertia Form, Link, server props, and manual error handling for forms, dialogs, tables, toasts.
npx claudepluginhub inertia-rails/skillsThis skill uses the workspace's default tool permissions.
shadcn-vue patterns adapted for Inertia.js + Rails + Vue 3. NOT Nuxt.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
shadcn-vue patterns adapted for Inertia.js + Rails + Vue 3. NOT Nuxt.
Before using a shadcn-vue example, ask:
useRouter, useFetch, <NuxtLink>) → Replace with Inertia router, server props, <Link>vee-validate + zod? → Replace with Inertia <Form> + name attributes. Inertia handles CSRF, errors, redirects, processing state.| shadcn-vue default (Nuxt) | Inertia equivalent |
|---|---|
useFetch / useAsyncData | Server-rendered props via controller |
useRouter() (Nuxt) | router from @inertiajs/vue3 |
<NuxtLink> | <Link> from @inertiajs/vue3 |
vee-validate + zod | Inertia <Form> component |
FormField, FormItem, FormMessage | Plain <Input name="..."> + errors.field |
useHead() (Nuxt) | <Head> from @inertiajs/vue3 |
NEVER use shadcn-vue's FormField, FormItem, FormLabel, FormMessage components —
they depend on vee-validate's form context internally and will crash without it.
Use plain shadcn-vue Input/Label/Select with name attributes inside Inertia <Form>,
and render errors from the scoped slot's errors object.
npx shadcn-vue@latest init. Add @/ resolve aliases to tsconfig.json if not present.
Do NOT add @/ resolve aliases to vite.config.ts — vite-plugin-ruby already provides them.
<Form>Use plain shadcn-vue Input/Label/Button with name attributes inside Inertia <Form>.
See inertia-rails-forms skill (+ references/vue.md) for full <Form> API.
The key pattern: Replace shadcn-vue's FormField/FormItem/FormMessage with plain
components + manual error display:
<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> requires name prop for Inertia <Form> integration:
<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>
Use <Link> (not <a>) for row links to preserve SPA navigation.
Flash config (flash_keys) is in inertia-rails-controllers. Flash access
(usePage().flash) is in inertia-rails-pages. This section covers toast UI wiring only.
MANDATORY — READ ENTIRE FILE when implementing flash-based toasts with Sonner:
references/flash-toast.md (~80 lines) — full useFlash
composable and Sonner toast provider. Do NOT load if only reading flash values without toast UI.
npx shadcn-vue@latest init generates CSS variables for light/dark and
@custom-variant dark (&:is(.dark *)); in your CSS (Tailwind v4). No extra
setup needed for the variables themselves.
CRITICAL — prevent flash of wrong theme (FOUC): Add an inline script in
<head> (before Vue hydrates):
<%# 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>
Use a useAppearance composable (light/dark/system modes, localStorage persistence,
matchMedia listener) instead of Nuxt color-mode. Toggle via .dark class on
<html> — no provider needed.
v-model does NOT work with Inertia <Form> — <Form> reads values from
input name attributes on submit, not from Vue's reactivity system. Using
v-model creates a second source of truth that <Form> ignores:
<!-- 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>
Use v-model only with useForm (where you explicitly manage form.name).
usePage() returns a reactive object — use computed() for derived values:
<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>
Without computed(), destructured values freeze at their initial state and
won't update after Inertia navigation.
@update:open vs @close for Dialog — shadcn-vue Dialog emits
update:open, not close. Using @close silently does nothing:
<!-- 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 |
|---|---|---|
FormField/FormMessage crash | Using shadcn-vue form components that depend on vee-validate | Replace with plain Input/Label + errors.field display |
Select value not submitted | Missing name prop | Add name="field" to <Select> |
| Dialog closes unexpectedly | Missing or wrong @update:open handler | Use @update:open="(open) => { if (!open) closeHandler() }" |
| Flash of wrong theme (FOUC) | Missing inline <script> in <head> | Add dark mode script before stylesheets |
v-model value not submitted | <Form> reads name attrs, not Vue reactive state | Use name attribute; reserve v-model for useForm only |
| Shared props stale after navigation | Destructured usePage() without computed() | Wrap derived values in computed(() => ...) |
inertia-rails-forms + references/vue.md (<Form> scoped slot, useForm)inertia-rails-controllers (flash_keys initializer)inertia-rails-pages + references/vue.md (usePage().flash)inertia-rails-pages + references/vue.md (router.get pattern)Load references/components.md (~200 lines) when building
shadcn-vue components beyond those shown above (Accordion, Sheet, Tabs, DropdownMenu,
AlertDialog with Inertia patterns).
Do NOT load components.md for basic Form, Select, Dialog, or Table usage —
the examples above are sufficient.