Getting started

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.

terminal
# already in your download: src/vui/vui.css
npm install -D tailwindcss @tailwindcss/vite
vite.config.js
import { 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(),
  ],
});
resources/css/app.css
@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.

resources/views/layouts/app.blade.php
<!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

Laravel already knows the locale, so setting 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

resources/views/components/vui/button.blade.php
@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>
using it
<x-vui.button variant="primary">Save</x-vui.button>

Badge

resources/views/components/vui/badge.blade.php
@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>
using it
<x-vui.badge variant="success">Active</x-vui.badge>

Card

resources/views/components/vui/card.blade.php
@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>
using it
<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.

terminal
# 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/
resources/views/invoices/index.blade.php
<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

It carries example content on purpose. Take it, replace the text with your own, keep the classes, and loop your rows inside it. The hand-written components above are the other shape, for the handful you want as real Blade components with @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-vue and you get the real components, datatable included, inside your Laravel app.

What we do and do not ship

There is no Composer package: @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.