Laravel and Blade
Laravel already ships Vite, Tailwind and Alpine, which is everything VUI needs. Import the theme, then write Blade components from the same class strings the React and Vue packages render, so a Blade page and a React page look like one product.
How do I add VUI to a Laravel app?
Two commands and one import. This assumes a Laravel 11 or 12 app with Vite, which is the default.
# already in your download: src/vui/vui.css
npm install -D tailwindcss @tailwindcss/viteimport { defineConfig } from "vite";
import laravel from "laravel-vite-plugin";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [
laravel({ input: ["resources/css/app.css", "resources/js/app.js"], refresh: true }),
tailwindcss(),
],
});@import "tailwindcss";
@import "@viliha/vui-css/theme.css";
/* Tailwind needs to see your Blade files to emit the utilities they use. */
@source "../views/**/*.blade.php";That last line matters more in Laravel than anywhere else: Tailwind scans source files for class names, and Blade templates are not JavaScript, so they have to be named explicitly.
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}"
dir="{{ in_array(app()->getLocale(), ['ar', 'he', 'fa', 'ur']) ? 'rtl' : 'ltr' }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-background text-foreground">
{{ $slot }}
</body>
</html>Right to left comes free here
dirfrom it is one expression. VUI's own shell uses logical properties, so the layout flips without any extra CSS.Blade components
Blade's anonymous components are the right shape for this: a file per component, @props for the variants, and $attributes->merge() so a caller can still pass a class. Below is each one written out. The class strings are the same ones the React and Vue components render.
Button
@props(['variant' => 'default', 'size' => 'default'])
@php
$base = 'inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-[var(--vui-icon-size)] [&_svg]:shrink-0';
$variants = [
'default' => 'border border-border bg-background text-foreground [&_svg]:text-[var(--vui-button-glyph,var(--muted-foreground))] hover:[&_svg]:text-[var(--vui-button-glyph-hover,var(--foreground))]',
'primary' => 'border border-transparent bg-[var(--button-primary)] text-[var(--button-primary-foreground)] shadow-[var(--button-shadow)] hover:bg-[var(--button-primary-hover)] [&_svg]:border-[var(--button-primary-hover)]',
'ghost' => 'hover:bg-accent hover:text-accent-foreground',
'destructive' => 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
];
$sizes = [
'default' => 'h-[var(--vui-control-h)] rounded-lg px-4',
'sm' => 'h-[var(--vui-control-h-sm)] rounded-sm px-2.5',
'lg' => 'h-[var(--vui-control-h)] rounded-lg px-4',
'icon' => 'size-8',
];
@endphp
<button {{ $attributes->merge(['type' => 'button', 'class' => $base.' '.$variants[$variant].' '.$sizes[$size]]) }}>
{{ $slot }}
</button><x-vui.button variant="primary">Save</x-vui.button>Badge
@props(['variant' => 'default'])
@php
$variants = [
'default' => 'border-transparent bg-primary/10 text-primary',
'muted' => 'border-transparent bg-muted text-muted-foreground',
'success' => 'border-transparent bg-success/10 text-success',
'warning' => 'border-transparent bg-warning/10 text-warning',
'destructive' => 'border-transparent bg-destructive/10 text-destructive dark:bg-destructive/20',
];
@endphp
<span {{ $attributes->merge(['class' => 'vui-badge '.$variants[$variant]]) }}>{{ $slot }}</span><x-vui.badge variant="success">Active</x-vui.badge>Card
@props(['title' => null])
<section {{ $attributes->merge(['class' => 'vui-card overflow-hidden']) }}>
@if ($title)
<header class="vui-dialog-header">
<h2 class="vui-dialog-title">{{ $title }}</h2>
</header>
@endif
<div class="px-5 py-4 text-sm leading-relaxed">{{ $slot }}</div>
</section><x-vui.card title="Team">Anything goes here.</x-vui.card>Everything else, as partials you do not write
@viliha/vui-laravel publishes a Blade partial for every component family, plus the compiled stylesheet and the three kilobytes of behaviour the interactive ones need. Four directories copied in is the whole install, and there is no Composer package and no service provider to keep in step with a Laravel major version.
# already in your download: the Laravel edition's own zip
cp -r node_modules/@viliha/vui-laravel/dist/resources/views/vui resources/views/
cp -r node_modules/@viliha/vui-laravel/dist/public/vui public/<x-vui-layout title="Invoices">
@include('vui.data-table')
</x-vui-layout>Nothing in that package is authored. Each partial is the markup the React edition renders, produced by renderToStaticMarkup and wrapped in a Blade header, so a Blade page and a React page are the same HTML rather than two attempts at it. Its own test asserts that byte for byte.
A partial is markup, not a parameterised component
@props.What about interactivity?
- Alpine ships with Laravel and covers dropdowns, dialogs and tabs:
x-data,x-show,x-trap,@click.outside. Style the result with the classes from the HTML page. - Livewire works the same way. VUI is CSS, so a component that re-renders server-side keeps its styling with no extra work.
- Inertia with React or Vue is the other path, and the most complete one: install
@viliha/vui-react(React) or@viliha/vui-vueand you get the real components, datatable included, inside your Laravel app.
What we do and do not ship
@viliha/vui-laravel ships on npm, because that is where the stylesheet it is generated from already lives, and a Blade partial is a file you copy rather than a dependency you resolve.Inertia is a different answer to a different question. An Inertia app renders React or Vue, so it wants
@viliha/vui-react or @viliha/vui-vue and the real components, datatable included. It does not want the Blade package, and we do not build the same thing twice: if you are on Inertia, this page is not the one you need.