# Inertia Rails Cookbook

> Recipes and patterns for common Inertia Rails use cases. Includes modal dialogs, shadcn/ui integration, search with filters, wizard flows, and other advanced patterns.

- **Type:** Skill
- **Install:** `agentstack add skill-cole-robertson-inertia-rails-skills-inertia-rails-cookbook`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [cole-robertson](https://agentstack.voostack.com/s/cole-robertson)
- **Installs:** 0
- **Category:** [Search](https://agentstack.voostack.com/c/search)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [cole-robertson](https://github.com/cole-robertson)
- **Source:** https://github.com/cole-robertson/inertia-rails-skills/tree/main/skills/inertia-rails-cookbook

## Install

```sh
agentstack add skill-cole-robertson-inertia-rails-skills-inertia-rails-cookbook
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Inertia Rails Cookbook

Practical recipes for common patterns and integrations in Inertia Rails applications.

## Working with the Official Starter Kits

The official starter kits provide a complete foundation. Here's how to customize them for your needs.

### Starter Kit Structure (React)

```
app/
├── controllers/
│   ├── application_controller.rb    # Shared data setup
│   ├── dashboard_controller.rb      # Example authenticated page
│   ├── home_controller.rb           # Public landing page
│   ├── sessions_controller.rb       # Login/logout
│   ├── users_controller.rb          # Registration
│   ├── identity/                    # Password reset
│   └── settings/                    # User settings
├── frontend/
│   ├── components/
│   │   ├── ui/                      # shadcn/ui components
│   │   ├── nav-main.tsx             # Main navigation
│   │   ├── app-sidebar.tsx          # Sidebar component
│   │   └── user-menu-content.tsx    # User dropdown
│   ├── hooks/
│   │   ├── use-flash.tsx            # Flash message hook
│   │   └── use-appearance.tsx       # Dark mode hook
│   ├── layouts/
│   │   ├── app-layout.tsx           # Main app layout
│   │   ├── auth-layout.tsx          # Auth pages layout
│   │   └── app/
│   │       ├── app-sidebar-layout.tsx
│   │       └── app-header-layout.tsx
│   ├── pages/
│   │   ├── dashboard/index.tsx      # Dashboard page
│   │   ├── home/index.tsx           # Landing page
│   │   ├── sessions/new.tsx         # Login page
│   │   ├── users/new.tsx            # Registration page
│   │   └── settings/                # Settings pages
│   └── types/
│       └── index.ts                 # Shared TypeScript types
```

### Adding a New Resource

**1. Generate the controller:**
```bash
bin/rails generate controller Products index show new create edit update destroy
```

**2. Create the page components:**

```tsx
// app/frontend/pages/products/index.tsx
import { Head, Link } from '@inertiajs/react'
import AppLayout from '@/layouts/app-layout'
import { Button } from '@/components/ui/button'
import {
  Table, TableBody, TableCell, TableHead, TableHeader, TableRow
} from '@/components/ui/table'

interface Product {
  id: number
  name: string
  price: number
}

interface Props {
  products: Product[]
}

export default function ProductsIndex({ products }: Props) {
  return (
    
      

      
        Products
        
          Add Product
        
      

      
        
          
            Name
            Price
            Actions
          
        
        
          {products.map((product) => (
            
              {product.name}
              ${product.price}
              
                Edit
              
            
          ))}
        
      
    
  )
}
```

**3. Update navigation:**

```tsx
// app/frontend/components/nav-main.tsx
const navItems = [
  { title: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
  { title: 'Products', href: '/products', icon: Package },  // Add this
  // ...
]
```

**4. Add route:**
```ruby
# config/routes.rb
resources :products
```

### Adding New shadcn/ui Components

The starter kit includes many components, but you can add more:

```bash
# Add a specific component
npx shadcn@latest add toast
npx shadcn@latest add calendar
npx shadcn@latest add data-table

# See all available components
npx shadcn@latest add
```

### Customizing the Layout

**Switch between sidebar and header layouts:**

```tsx
// app/frontend/layouts/app-layout.tsx
import AppSidebarLayout from '@/layouts/app/app-sidebar-layout'
import AppHeaderLayout from '@/layouts/app/app-header-layout'

// Use sidebar (default)
export default function AppLayout({ children }: Props) {
  return {children}
}

// Or use header layout
export default function AppLayout({ children }: Props) {
  return {children}
}
```

### Extending Types

```tsx
// app/frontend/types/index.ts
export interface User {
  id: number
  name: string
  email: string
  avatar_url: string | null
}

// Add your own types
export interface Product {
  id: number
  name: string
  description: string
  price: number
  created_at: string
}

export interface PageProps {
  auth: {
    user: User | null
  }
  flash: {
    success?: string
    error?: string
  }
}
```

### Using the Flash Hook

The starter kit includes a flash message system with Sonner toasts:

```tsx
// Already set up in the layout, just use flash in your controller
class ProductsController Dashboard content
}

// Pass static props to the layout
Dashboard.layout = [AppLayout, { title: 'Dashboard', breadcrumbs: ['Home', 'Dashboard'] }]
```

### Dynamic Layout Props

```tsx
import { useLayoutProps } from '@inertiajs/react'
import AppLayout from '@/layouts/app-layout'

export default function UserProfile({ user }) {
  // Set layout props dynamically
  useLayoutProps({ title: user.name, breadcrumbs: ['Users', user.name] })

  return {user.name}'s profile
}

UserProfile.layout = AppLayout
```

### In the Layout

```tsx
// app/frontend/layouts/app-layout.tsx
import { useLayoutProps } from '@inertiajs/react'

export default function AppLayout({ children }) {
  const { title, breadcrumbs } = useLayoutProps()

  return (
    
      
        {title}
        {breadcrumbs?.join(' > ')}
      
      {children}
    
  )
}
```

---

## Inertia Modal - Render Pages as Dialogs

The `inertia_rails-contrib` gem and `@inertiaui/modal` package let you render any Inertia page as a modal dialog.

### Installation

```bash
# Ruby gem (optional, for base_url helper)
bundle add inertia_rails-contrib

# NPM package (Vue)
npm install @inertiaui/modal-vue

# NPM package (React)
npm install @inertiaui/modal-react
```

### Setup (React)

```javascript
// app/frontend/entrypoints/application.js
import { createInertiaApp } from '@inertiajs/react'
import { createRoot } from 'react-dom/client'
import { renderApp } from '@inertiaui/modal-react'

createInertiaApp({
  resolve: (name) => {
    const pages = import.meta.glob('../pages/**/*.tsx', { eager: true })
    return pages[`../pages/${name}.tsx`]
  },
  setup({ el, App, props }) {
    createRoot(el).render(renderApp(App, props))
  },
})
```

### Tailwind Configuration

```javascript
// tailwind.config.js (v3)
module.exports = {
  content: [
    // ... your content paths
    './node_modules/@inertiaui/modal-react/src/**/*.{js,jsx,ts,tsx}',
  ],
}
```

```css
/* For Tailwind v4 */
@import "tailwindcss";
@source '../../../node_modules/@inertiaui/modal-react';
```

### Basic Usage

**Open a page as modal:**
```jsx
import { ModalLink } from '@inertiaui/modal-react'

export default function UsersList() {
  return (
    
      Create User
    
  )
}
```

**Wrap page content in Modal:**
```jsx
// pages/users/create.tsx
import { Modal } from '@inertiaui/modal-react'

export default function CreateUser({ roles }) {
  return (
    
      Create User
      
    
  )
}
```

### Modal with Base URL

Enable URL updates and browser history:

**Controller:**
```ruby
class UsersController 
  Create User

```

Now the URL changes to `/users/create` when opened, supports browser back button, and can be bookmarked.

### Slideover Variant

```jsx

  User Details
  {/* Content slides in from the side */}

```

### Nested Modals

```jsx

  Edit User
  

  {/* Open another modal from within */}
  
    Add New Role
  

```

### Closing Modals

```jsx
import { Modal } from '@inertiaui/modal-react'

export default function EditUser({ onClose }) {
  return (
    
      Cancel
    
  )
}
```

---

## Integrating shadcn/ui

Use shadcn/ui components with Inertia Rails for a polished UI.

### Setup (React)

```bash
# Initialize shadcn/ui
npx shadcn@latest init

# Add components
npx shadcn@latest add button input card form
```

### Form with shadcn/ui

```jsx
import { useForm } from '@inertiajs/react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'

export default function LoginForm() {
  const { data, setData, post, processing, errors } = useForm({
    email: '',
    password: '',
  })

  function submit(e) {
    e.preventDefault()
    post('/login')
  }

  return (
    
      
        Login
      
      
        
          
            Email
             setData('email', e.target.value)}
              placeholder="you@example.com"
            />
            {errors.email && (
              {errors.email}
            )}
          

          
            Password
             setData('password', e.target.value)}
            />
          

          
            {processing ? 'Signing in...' : 'Sign in'}
          
        
      
    
  )
}
```

### Data Table with Sorting and Filtering

```jsx
import { router, usePage } from '@inertiajs/react'
import { useState, useCallback } from 'react'
import {
  Table, TableBody, TableCell, TableHead, TableHeader, TableRow
} from '@/components/ui/table'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Link } from '@inertiajs/react'
import { useDebouncedCallback } from 'use-debounce'

export default function UsersTable({ users, filters }) {
  const [search, setSearch] = useState(filters.search || '')
  const [sort, setSort] = useState(filters.sort || 'name')
  const [direction, setDirection] = useState(filters.direction || 'asc')

  const debouncedSearch = useDebouncedCallback((value) => {
    router.get('/users', { search: value, sort, direction }, {
      preserveState: true,
      replace: true,
    })
  }, 300)

  function toggleSort(column) {
    const newDirection = sort === column && direction === 'asc' ? 'desc' : 'asc'
    setSort(column)
    setDirection(newDirection)
    router.get('/users', { search, sort: column, direction: newDirection }, {
      preserveState: true,
      replace: true,
    })
  }

  return (
    
       {
          setSearch(e.target.value)
          debouncedSearch(e.target.value)
        }}
        placeholder="Search users..."
        className="max-w-sm"
      />

      
        
          
            
               toggleSort('name')}>
                Name {sort === 'name' && (direction === 'asc' ? '↑' : '↓')}
              
            
            
               toggleSort('email')}>
                Email {sort === 'email' && (direction === 'asc' ? '↑' : '↓')}
              
            
            Actions
          
        
        
          {users.map((user) => (
            
              {user.name}
              {user.email}
              
                Edit
              
            
          ))}
        
      
    
  )
}
```

---

## Search with Filters

### Controller

```ruby
class UsersController  applyFilters(), 300)

  function clearFilters() {
    setSearch('')
    setRole('')
    setActive('')
    router.get('/users', {}, { preserveState: true, replace: true })
  }

  return (
    
      
         { setSearch(e.target.value); debouncedSearch() }}
          placeholder="Search..."
          className="input"
        />

         { setRole(e.target.value); applyFilters({ role: e.target.value }) }} className="select">
          All Roles
          Admin
          User
        

         { setActive(e.target.value); applyFilters({ active: e.target.value }) }} className="select">
          All Status
          Active
          Inactive
        

        Clear
      

      
      
    
  )
}
```

---

## Multi-Step Wizard

### Controller

```ruby
class OnboardingController 
      {/* Progress indicator */}
      
        {Array.from({ length: total_steps }, (_, i) => i + 1).map((i) => (
          
            {i}
          
        ))}
      

      
        {typeof children === 'function' ? children({ form }) : children}

        
          {step > 1 && (
            Back
          )}
          
            {step === total_steps ? 'Complete' : 'Next'}
          
        
      
    
  )
}
```

---

## Flash Messages with Toast

### Shared Data Setup

```ruby
# app/controllers/application_controller.rb
class ApplicationController  {
    {
      success: flash.notice,
      error: flash.alert,
      info: flash[:info],
      warning: flash[:warning]
    }.compact
  }
end
```

### Toast Component (React)

```jsx
// components/FlashMessages.tsx
import { usePage } from '@inertiajs/react'
import { useEffect, useState } from 'react'

interface Toast {
  id: number
  type: string
  message: string
}

export default function FlashMessages() {
  const { flash } = usePage().props
  const [toasts, setToasts] = useState([])

  useEffect(() => {
    if (!flash) return

    Object.entries(flash).forEach(([type, message]) => {
      if (message) {
        const id = Date.now() + Math.random()
        setToasts((prev) => [...prev, { id, type, message: message as string }])

        setTimeout(() => {
          setToasts((prev) => prev.filter((t) => t.id !== id))
        }, 5000)
      }
    })
  }, [flash])

  const colorMap: Record = {
    success: 'bg-green-500 text-white',
    error: 'bg-red-500 text-white',
    info: 'bg-blue-500 text-white',
    warning: 'bg-yellow-500 text-black',
  }

  return (
    
      {toasts.map((toast) => (
        
          {toast.message}
        
      ))}
    
  )
}
```

### Usage in Layout

```jsx
// layouts/AppLayout.tsx
import FlashMessages from '@/components/FlashMessages'

export default function AppLayout({ children }) {
  return (
    
      
      {/* ... */}
      {children}
    
  )
}
```

---

## Flash API (v3)

Inertia.js v3 introduces a dedicated Flash API for richer flash data beyond simple key-value strings.

### Server-Side

```ruby
class PostsController  {
      router.post(flash.undo.url)
    })
  }
}

// Client-side flash (no server roundtrip)
router.flash({ notice: 'Saved locally' })
```

### Configure Flash Keys

```ruby
InertiaRails.configure do |config|
  config.flash_keys = %i[notice alert success error warning info]
end
```

---

## Confirmation Dialogs

### Reusable Confirm Component

```jsx
// components/ConfirmDialog.tsx
import { useState, useCallback, useRef } from 'react'

interface ConfirmOptions {
  title?: string
  message?: string
  confirmText?: string
  cancelText?: string
  destructive?: boolean
}

export function useConfirm() {
  const [isOpen, setIsOpen] = useState(false)
  const [options, setOptions] = useState({})
  const resolveRef = useRef void) | null>(null)

  const confirm = useCallback((opts: ConfirmOptions = {}) => {
    setOptions({
      title: 'Are you sure?',
      message: 'This action cannot be undone.',
      confirmText: 'Confirm',
      cancelText: 'Cancel',
      destructive: false,
      ...opts,
    })
    setIsOpen(true)

    return new Promise((resolve) => {
      resolveRef.current = resolve
    })
  }, [])

  function handleConfirm() {
    setIsOpen(false)
    resolveRef.current?.(true)
  }

  function handleCancel() {
    setIsOpen(false)
    resolveRef.current?.(false)
  }

  const ConfirmDialog = isOpen ? (
    
      
      
        {options.title}
        {options.message}
        
          
            {options.cancelText}
          
          
            {options.confirmText}
          
        
      
    
  ) : null

  return { confirm, ConfirmDialog }
}
```

### Usage

```jsx
import { router } from '@inertiajs/react'
import { useConfirm } from '@/components/ConfirmDialog'

export default function UserRow({ user }) {
  const { confirm, ConfirmDialog } = useConfirm()

  async function deleteUser() {
    const confirmed = await confirm({
      title: 'Delete User',
      message: `Are you sure you want to delete ${user.name}?`,
      confirmText: 'Delete',
      destructive: true,
    })

    if (confirmed) {
      router.delete(`/users/${user.id}`)
    }
  }

  return (
    
      Delete
      {ConfirmDialog}
    
  )
}
```

---

## Handling Rails Validation Error Types

Rails returns different error formats. Handle

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [cole-robertson](https://github.com/cole-robertson)
- **Source:** [cole-robertson/inertia-rails-skills](https://github.com/cole-robertson/inertia-rails-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-cole-robertson-inertia-rails-skills-inertia-rails-cookbook
- Seller: https://agentstack.voostack.com/s/cole-robertson
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
