mcpField
S
shadcn-inertia

shadcn-inertia

github.com/inertia-railsDevelopment70433 installsUpdated 37w ago
Repository

About shadcn-inertia

shadcn-inertia is a skill for adapting shadcn/ui components to Inertia.js, Rails, and React projects. It explains how to replace Next.js and react-hook-form conventions with Inertia patterns, including forms, navigation, flash toasts, and theme handling.

Key features

  1. 01Maps Next.js patterns to Inertia
  2. 02Guidance for Inertia form fields
  3. 03SPA navigation and URL dialogs
  4. 04Flash-message toast wiring
  5. 05Dark-mode setup without next-themes

Use cases

  1. 01Adapt a shadcn form for Inertia
  2. 02Add sorting to a server-side table
  3. 03Wire flash messages to Sonner toasts
  4. 04Prevent a dark-mode theme flash

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/inertia-rails/skills --skill shadcn-inertia

After installing, type/shadcn-inertiain Claude Code to confirm it loaded.

Bundled filesreferences/components.mdreferences/flash-toast.md

Details

From the project README

shadcn/ui for Inertia Rails

shadcn/ui patterns adapted for Inertia.js + Rails + React. NOT Next.js.

Before using a shadcn example, ask:

  • Does it use react-hook-form + zod? → Replace with Inertia <Form> + name attributes. Inertia handles CSRF, errors, redirects, processing state — react-hook-form would fight all of this.
  • Does it use 'use client'? → Remove it. Inertia has no RSC — all components are client components.
  • Does it use next/link, next/head, useRouter()? → Replace with Inertia <Link>, <Head>, router.

Key Differences from Next.js Defaults

shadcn default (Next.js)Inertia equivalent
'use client' directiveRemove — not needed (no RSC)
react-hook-form + zodInertia <Form> component
FormField, FormItem, FormMessagePlain <Input name="..."> + errors.field
next-themesCSS class strategy + @custom-variant
useRouter() (Next)router from @inertiajs/react
next/link<Link> from @inertiajs/react
next/head<Head> from @inertiajs/react

NEVER use shadcn's FormField, FormItem, FormLabel, FormMessage components — they depend on react-hook-form's useFormContext internally and will crash without it. Use plain shadcn Input/Label/Select with name attributes inside Inertia <Form>, and render errors from the render function's errors object (see examples below).

Setup

npx shadcn@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.

shadcn Inputs in Inertia <Form>

Use plain shadcn Input/Label/Button with name attributes inside Inertia <Form>. See inertia-rails-forms skill for full <Form> API — this section covers shadcn-specific adaptation only.

The key pattern: Replace shadcn's FormField/FormItem/FormMessage with plain components + manual error display:

// shadcn error display pattern (replaces FormMessage):
<Label htmlFor="name">Name</Label>
<Input id="name" name="name" />
{errors.name && <p className="text-sm text-destructive">{errors.name}</p>}

<Select> requires name prop for Inertia <Form> integration — shadcn examples omit it because react-hook-form manages values differently:

<Select name="role" defaultValue="member">
  <SelectTrigger><SelectValue placeholder="Select role" /></SelectTrigger>
  <SelectContent>
    <SelectItem value="admin">Admin</SelectItem>
    <SelectItem value="member">Member</SelectItem>
  </SelectContent>
</Select>

Dialog with Inertia Navigation

import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { router } from '@inertiajs/react'

function UserDialog({ open, user }: { open: boolean; user: User }) {
  return (
    <Dialog
      open={open}
      onOpenChange={(isOpen) => {
        if (!isOpen) {
          router.replaceProp('show_dialog', false)
        }
      }}
    >
      <DialogContent>
        <DialogHeader>
          <DialogTitle>{user.name}</DialogTitle>
        </DialogHeader>
        {/* content */}
      </DialogContent>
    </Dialog>
  )
}

Table with Server-Side Sorting

shadcn <Table> renders normally. The Inertia-specific part is sorting via router.get:

const handleSort = (column: string) => {
  router.get('/users', { sort: column }, { preserveState: true })
}

<TableHead onClick={() => handleSort('name')} className="cursor-pointer">
  Name {sort === 'name' && '↑'}
</TableHead>

Use <Link> (not <a>) for row links to preserve SPA navigation.

Toast with Flash Messages

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 hook and Sonner toast provider. Do NOT load if only reading flash values without toast UI.

Key gotcha: flash_keys in the Rails initializer MUST match your FlashData TypeScript type — do NOT use success/error unless you also update both.

Dark Mode (No next-themes)

npx shadcn@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): Next.js handles this automatically; Inertia does NOT. Add an inline script in <head> (before React hydrates) and call initializeTheme() in your Inertia entrypoint:

<%# 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>
// app/frontend/entrypoints/inertia.tsx
import { initializeTheme } from '@/hooks/use-appearance'
initializeTheme() // must run before createInertiaApp

Use a useAppearance hook (light/dark/system modes, localStorage persistence, matchMedia listener) instead of next-themes. Toggle via .dark class on <html> — no provider needed.

Troubleshooting

SymptomCauseFix
FormField/FormMessage crashUsing shadcn form components that depend on react-hook-formReplace with plain Input/Label + errors.field display
Select value not submittedMissing name propAdd name="field" to <Select> — shadcn examples omit it
Dialog closes unexpectedlyMissing or wrong onOpenChange handlerUse onOpenChange={(open) => { if (!open) closeHandler() }}
Flash of wrong theme (FOUC)Missing inline <script> in <head>Add dark mode script before stylesheets (see Dark Mode section)

Related Skills

  • Form component → inertia-rails-forms (<Form> render function, useForm)
  • Flash config → inertia-rails-controllers (flash_keys initializer)
  • Flash access → inertia-rails-pages (usePage().flash)
  • URL-driven dialogs → inertia-rails-pages (router.get pattern)

References

Load references/components.md (~300 lines) when building shadcn 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.

Comments

Markdown supported