connora/laravel-primevue-starter-kit
A Laravel starter kit pairing Vue 3 and PrimeVue with Inertia.js, including prebuilt authentication, layouts, and UI components so you can skip boilerplate and start building admin panels or SaaS apps faster.
This is a starter kit, not a runtime dependency — you don't composer require it into an existing app. You scaffold a new project from it and then own every line of the resulting codebase.
Scaffold:
# Via the Laravel installer (recommended)
laravel new my-app --using=connora/laravel-primevue-starter-kit
# Or clone directly
git clone https://github.com/connorabbas/laravel-primevue-starter-kit my-app
cd my-app && rm -rf .git && git init
Bootstrap:
composer install
cp .env.example .env
php artisan key:generate
npm install
php artisan migrate
npm run dev # Vite HMR
php artisan serve
If the kit ships a database seeder, run php artisan migrate --seed to get a demo user instead of registering by hand.
Where to look first, in order:
resources/js/app.ts (or app.js) — PrimeVue plugin registration, theme preset, and which services (Toast, ConfirmationService) are mounted globally. This tells you what's available everywhere.resources/js/Layouts/ — the authenticated layout is where navigation, flash-message handling, and <Toast /> / <ConfirmDialog /> live. Most UI wiring decisions trace back here.resources/js/Pages/ — the Inertia page components. Follow one CRUD-ish page end to end to see the house style for forms and tables.routes/web.php + app/Http/Controllers/ — how pages are rendered via Inertia::render().resources/css/app.css and tailwind.config.js — how Tailwind and PrimeVue's theme layer are reconciled (this is the most common source of confusion later).First use case: change the dashboard. Add a route, a controller action returning Inertia::render('Dashboard', ['stats' => ...]), and edit the Vue page to render those props inside a PrimeVue <Card> and <DataTable>. That single loop exercises routing, Inertia props, layout, theme, and component imports — the four things you'll touch daily.
Page = controller + Inertia render + Vue SFC. Stick to this trio; don't add API routes for things the UI needs. Props are your data contract.
// app/Http/Controllers/ProjectController.php
public function index()
{
return Inertia::render('Projects/Index', [
'projects' => Project::latest()->paginate(15),
]);
}
Forms: Inertia useForm driven by PrimeVue inputs. The pattern that scales is a single form object bound with v-model, :invalid="!!form.errors.x" on PrimeVue fields, and <Message> beneath. PrimeVue's invalid prop is the idiomatic error-visual hook — don't hand-roll red borders.
<script setup>
import { useForm } from '@inertiajs/vue3'
const form = useForm({ name: '', description: '' })
const submit = () => form.post(route('projects.store'), { onSuccess: () => form.reset() })
</script>
<template>
<form @submit.prevent="submit">
<InputText v-model="form.name" :invalid="!!form.errors.name" />
<Message v-if="form.errors.name" severity="error" size="small" variant="simple">
{{ form.errors.name }}
</Message>
<Button type="submit" label="Save" :loading="form.processing" />
</form>
</template>
Flash messages go through Toast, set once. Have the shared layout watch $page.props.flash and call useToast().add(...). Every controller then just does ->with('success', 'Saved') and the UI is consistent without per-page wiring.
CRUD index pages: <DataTable> + <Column> + a <Dialog> for create/edit. Keep the dialog's visibility in local ref, the record in a local ref, and submit with useForm. Avoid editing rows in place — the dialog pattern is easier to test and matches the kit's styling.
Destructive actions: ConfirmationService. Wrap the page in <ConfirmDialog /> (often already in the layout) and call confirm.require({...}) before a router.delete(...). This is the kit's expected UX; bypassing it for a window.confirm will look out of place.
Theming via definePreset. Customize PrimeVue tokens in app.ts rather than overriding component CSS:
import { definePreset } from '@primevue/themes'
import Aura from '@primevue/themes/aura'
const Brand = definePreset(Aura, {
semantic: { primary: { 500: '#6366f1', 600: '#4f46e5' } },
})
app.use(PrimeVue, { theme: { preset: Brand, options: { darkModeSelector: '.dark' } } })
Then drive dark mode by toggling .dark on <html> and persisting the choice in localStorage.
Add components on demand. Import PrimeVue components in the SFC that uses them rather than registering everything globally — the kit's bundle assumes tree-shaking. Reserve app.ts registration for genuinely universal pieces (Button, InputText) and services.
Server-side validation stays server-side. Use Form Requests, return back()->withErrors(), and let useForm surface them. Don't duplicate rules in Vue; you'll drift.
You own the code — there are no upstream updates. This is a scaffold; composer update won't pull fixes from the starter kit. Pin a branch or fork it internally if you plan to reuse it across projects. Budget time for Laravel/Inertia major-version upgrades yourself.
Tailwind Preflight vs. PrimeVue. The most frequent "why does my button look broken" moment. Tailwind's reset strips base styles PrimeVue components rely on. The kit should already reconcile this (commonly via tailwindcss-primeui and ordering @layer rules), but if you add Tailwind plugins or change import order in app.css, expect regressions in PrimeVue components. Keep the PrimeVue theme import after Tailwind's base layer.
Toast and ConfirmationService are invisible until mounted. If useToast().add() silently does nothing, the <Toast /> component isn't in the active layout — a classic symptom after adding a new layout (e.g. a guest or print layout) that doesn't include the global overlays.
Dark mode needs three things in sync: the .dark class on <html>, PrimeVue's darkModeSelector option, and Tailwind's darkMode: 'class'. Mismatch any one and you get half-themed pages.
v-model + PrimeVue + useForm reactivity. PrimeVue inputs write back fine to form properties, but form.errors only updates after a visit. If you're doing client-side dirty tracking, use form.isDirty rather than comparing snapshots manually.
Bundle size creeps fast. PrimeVue is large; DataTable alone is heavy. If you globally register everything in app.ts "for convenience," you'll get a multi-hundred-KB initial chunk. Prefer per-page imports and check npm run build output when adding components.
SSR is not free. If the kit is configured for Inertia SSR, PrimeVue services (Toast/Confirm) and theme injection need SSR-safe handling. Test npm run build && php artisan inertia:start-ssr before deploying; hydration mismatches from theme class toggling are common.
Routes in Vue come from Ziggy. route('projects.store') fails at runtime if you add a Laravel route and Vite hasn't regenerated Ziggy's manifest. In dev it's usually automatic; in production, ensure php artisan ziggy:generate runs in your build pipeline.
Config quirks worth knowing up front: .env changes need php artisan config:clear in local dev if you're caching; vite.config.ts alias changes must be mirrored in tsconfig.json paths; and if you see stale UI, kill the Vite server — Inertia + HMR occasionally serves a cached page component.
Extension points to favor: add shared UI to Layouts/AppLayout, add form primitives under Components/, and put cross-cutting Inertia props in HandleInertiaRequests::share(). Those three are the seams the kit is designed around — fighting them creates one-off patterns that make the codebase hard to hand off.
How can I help you explore Laravel packages today?