Frontend Development
Unified skill for implementing frontend interfaces with the project's full stack: React 19 + TypeScript, Inertia.js v2, Tailwind CSS v4, Radix UI, and Wayfinder.
1. Before Implementation
- •Check
docs/design/for UI design specifications (if they exist) - •Check existing components in
resources/js/components/ui/before creating new ones - •Check sibling pages/components for patterns to follow
- •Use
search-docsfor version-specific documentation on any framework
Project Structure
resources/js/ ├── components/ # Shared components │ ├── ui/ # UI primitives (Button, Card, Dialog, etc.) │ └── [domain].tsx # Domain components (app-sidebar, nav-main, etc.) ├── hooks/ # Custom React hooks ├── layouts/ # Page layouts │ ├── app/ # App layouts (sidebar, header) │ ├── auth/ # Auth layouts (simple, split, card) │ └── settings/ # Settings layout ├── pages/ # Inertia page components │ ├── auth/ # Auth pages │ ├── settings/ # Settings pages │ └── [domain]/ # Domain pages ├── lib/ # Utilities (cn, toUrl) ├── types/ # TypeScript type definitions └── actions/ # Wayfinder generated (do not edit)
2. Component Patterns
Styling Utilities
Always use the cn() utility for conditional/merged classes:
import { cn } from '@/lib/utils'
<div className={cn('base-classes', conditional && 'active-class', className)} />
Component Variants with CVA
Use class-variance-authority for components with visual variants:
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const badgeVariants = cva(
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground',
secondary: 'bg-secondary text-secondary-foreground',
destructive: 'bg-destructive text-white',
outline: 'border border-input text-foreground',
},
},
defaultVariants: { variant: 'default' },
}
)
function Badge({ className, variant, ...props }: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants>) {
return <span className={cn(badgeVariants({ variant }), className)} {...props} />
}
Radix UI Primitives
The project uses Radix UI for accessible headless components. Existing primitives in components/ui/:
| Primitive | File | Radix Package |
|---|---|---|
| Dialog/Modal | ui/dialog.tsx | @radix-ui/react-dialog |
| Dropdown Menu | ui/dropdown-menu.tsx | @radix-ui/react-dropdown-menu |
| Select | ui/select.tsx | @radix-ui/react-select |
| Tooltip | ui/tooltip.tsx | @radix-ui/react-tooltip |
| Avatar | ui/avatar.tsx | @radix-ui/react-avatar |
| Checkbox | ui/checkbox.tsx | @radix-ui/react-checkbox |
| Collapsible | ui/collapsible.tsx | @radix-ui/react-collapsible |
| Toggle | ui/toggle.tsx | @radix-ui/react-toggle |
| Sidebar | ui/sidebar.tsx | Custom (uses Sheet internally) |
| Sheet | ui/sheet.tsx | @radix-ui/react-dialog |
When needing a new primitive: check if Radix UI has it, create wrapper in components/ui/, follow existing patterns (data-slot, cn utility).
For detailed component patterns and Radix integration, see references/component-patterns.md.
3. Inertia.js v2
Page Components
Pages live in resources/js/pages/. Receive props from Laravel controllers via Inertia::render().
export default function UsersIndex({ users }: { users: App.Models.User[] }) {
return (
<div>
<h1>Users</h1>
{users.map(user => <div key={user.id}>{user.name}</div>)}
</div>
)
}
Navigation with Link
Use <Link> for client-side navigation (never <a>):
import { Link } from '@inertiajs/react'
<Link href="/users">Users</Link>
<Link href="/logout" method="post" as="button">Logout</Link>
<Link href="/users" prefetch>Users (prefetched)</Link>
Forms — Form Component (Recommended)
import { Form } from '@inertiajs/react'
<Form action="/users" method="post" resetOnSuccess>
{({ errors, processing, wasSuccessful }) => (
<>
<input type="text" name="name" />
{errors.name && <div className="text-destructive text-sm">{errors.name}</div>}
<button type="submit" disabled={processing}>
{processing ? 'Saving...' : 'Save'}
</button>
{wasSuccessful && <div>Saved!</div>}
</>
)}
</Form>
Render props: errors, hasErrors, processing, progress, wasSuccessful, recentlySuccessful, clearErrors, resetAndClearErrors, defaults, isDirty, reset, submit.
Forms — useForm Hook
For programmatic control:
import { useForm } from '@inertiajs/react'
const { data, setData, post, processing, errors, reset } = useForm({
name: '', email: '',
})
function submit(e: React.FormEvent) {
e.preventDefault()
post('/users', { onSuccess: () => reset() })
}
Deferred Props (v2)
Load data after initial page render with skeleton fallback:
export default function Dashboard({ stats }: { stats?: DashboardStats }) {
if (!stats) return <Skeleton className="h-32 w-full" />
return <StatsGrid stats={stats} />
}
Polling (v2)
import { router } from '@inertiajs/react'
import { useEffect } from 'react'
useEffect(() => {
const interval = setInterval(() => router.reload({ only: ['stats'] }), 5000)
return () => clearInterval(interval)
}, [])
WhenVisible — Infinite Scroll (v2)
import { WhenVisible } from '@inertiajs/react'
<WhenVisible data="users" params={{ page: users.current_page + 1 }}
fallback={<div>Loading more...</div>} />
Shared Data & Persistent Layouts
Persistent layouts avoid re-mounting on navigation:
import AppLayout from '@/layouts/app-layout'
function Dashboard({ stats }) {
return <div>...</div>
}
Dashboard.layout = (page: React.ReactNode) => <AppLayout>{page}</AppLayout>
export default Dashboard
For detailed Inertia patterns, use search-docs with queries like shared data, persistent layouts, partial reloads, merge props.
4. Wayfinder — Backend Routes
Import TypeScript route functions generated from Laravel routes:
// Controller action imports (preferred for tree-shaking)
import { show, store } from '@/actions/App/Http/Controllers/PostController'
// Named route imports
import { show as postShow } from '@/routes/post'
// Methods
show.url(1) // "/posts/1"
show(1) // { url: "/posts/1", method: "get" }
store.form() // { action: "/posts", method: "post" }
// With Inertia Form
<Form {...store.form()}>...</Form>
// Query params
show(1, { query: { page: 2 } })
Regenerate after route changes: php artisan wayfinder:generate --no-interaction
5. Tailwind CSS v4
Key Differences from v3
- •Import:
@import "tailwindcss"(not@tailwinddirectives) - •Config: CSS
@themedirective (nottailwind.config.js) - •Opacity:
bg-black/50(notbg-opacity-50) - •Shrink/Grow:
shrink-*/grow-*(notflex-shrink-*/flex-grow-*)
Common Layout Patterns
<!-- Flex with gap --> <div class="flex items-center justify-between gap-4">...</div> <!-- Responsive grid --> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">...</div> <!-- Stack --> <div class="flex flex-col gap-4">...</div>
Dark Mode
Use dark: variant, match existing patterns:
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">...</div>
Responsive
Breakpoints: sm (640), md (768), lg (1024), xl (1280), 2xl (1536). Mobile-first.
For detailed Tailwind patterns, use search-docs with relevant queries.
6. TypeScript Conventions
- •Define page prop types inline or in
resources/js/types/ - •Use
App.Models.*namespace for model types - •All components must have typed props
- •Use
React.ComponentProps<'element'>for extending HTML element props
7. Quality Checklist
Before considering frontend work complete:
- • Uses existing
ui/components (no reinventing) - • Follows sibling page/component patterns
- • Loading states for deferred props (skeleton/spinner)
- • Error states for forms (inline errors)
- • Empty states where data can be empty
- • Dark mode support (if project uses it)
- • Responsive at defined breakpoints
- • Accessible (keyboard nav, ARIA labels, focus management)
- • Uses
cn()for conditional classes - • Uses Wayfinder for backend route references
- •
npm run lint && npm run typespasses
Common Pitfalls
- •Using
<a>instead of<Link>(breaks SPA navigation) - •Using
<form>withoute.preventDefault()(use<Form>component instead) - •Forgetting loading states for deferred props (
undefinedinitially) - •Creating new components that duplicate existing
ui/primitives - •Hard-coding route URLs instead of using Wayfinder
- •Using deprecated Tailwind v3 utilities (
bg-opacity-*,flex-shrink-*,@tailwind) - •Missing
keyprop in.map()iterations - •Not handling
processingstate in form submissions