Leaf 5 โ Views & Frontend Reference โ
Overview โ
| Engine | Speed | Features | Use Case |
|---|---|---|---|
| BareUI | โก๏ธ Fast | Basic (pure PHP) | Simple/performance-critical views |
| Blade | Moderate | Rich (@ directives) | Full-featured templating |
| Inertia | โ | React/Vue/Svelte | SPA-style frontend frameworks |
BareUI โ
Lightweight, pure PHP templating. No compilation, no caching โ just speed.
leaf install bareuiSetup (Basic app only โ MVC pre-configured) โ
app()->template()->config('path', './views');Template Files โ
Extension: .view.php
<!-- welcome.view.php -->
<h1>Hello <?php echo $name; ?></h1>
<?php if (count($items) > 0) : ?>
<ul>
<?php foreach ($items as $item) : ?>
<li><?php echo $item['name']; ?></li>
<?php endforeach; ?>
</ul>
<?php else : ?>
<p>No items</p>
<?php endif; ?>Render โ
response()->render('welcome');
response()->render('welcome', ['name' => 'Michael', 'items' => $items]);Sub-templates / Partials โ
All BareUI templates have access to $template:
<?php echo $template::render('partials/header'); ?>
<?php echo $template::render('partials/footer', ['year' => date('Y')]); ?>XSS protection is handled automatically by Leaf Anchor when using Leaf's request functions.
Blade โ
Laravel's templating engine. Pre-installed in MVC.
leaf install blade@v4 # Basic appRender โ
response()->render('hello', ['name' => 'Michael']); // hello.blade.phpCore Directives โ
{{-- Output (auto-escaped) --}}
{{ $name }}
{{-- Unescaped output --}}
{!! $html !!}
{{-- PHP blocks --}}
@php $x = 1; @endphp
{{-- Conditionals --}}
@if ($condition) ... @elseif ($other) ... @else ... @endif
{{-- Loops --}}
@foreach ($items as $item) ... @endforeach
@for ($i = 0; $i < 10; $i++) ... @endfor
@while ($condition) ... @endwhileLeaf-specific Directives โ
{{-- CSRF token --}}
<form method="POST">
@csrf
...
</form>
{{-- Auth state --}}
@auth // user is logged in @endauth
@guest // user is not logged in @endguest
{{-- Roles & permissions --}}
@is('admin') ... @endis
@can('edit articles') ... @endcan
{{-- Environment --}}
@env('production') ... @else ... @endenv
{{-- Null check --}}
@isNull($variable) ... @else ... @endisNull
{{-- Session --}}
@session('status')
<div>{{ $value }}</div>
@endsession
{{-- Flash messages --}}
@flash('status')
<div>{{ $message }}</div>
@endflash
{{-- JSON --}}
<script>var app = @json($array);</script>
{{-- Vite assets --}}
@vite('app.js')
@vite(['app.js', 'app.css'])
{{-- Alpine.js --}}
<head>
@alpine
</head>
{{-- Toast notifications --}}
<body>
...
@toastContainer
</body>
{{-- i18n --}}
<h1>@lingo('hero.title')</h1>Conditional HTML Attributes โ
@class(['p-4', 'font-bold' => $isActive, 'bg-red' => $hasError])
@style(['background-color: red', 'font-weight: bold' => $isActive])
@checked($isActive)
@selected($shouldBeSelected)
@disabled($shouldBeDisabled)
@readonly($shouldBeReadonly)
@required($shouldBeRequired)Custom Directives โ
app()->blade()->directive('datetime', function ($expression) {
return "<?php echo tick({$expression})->format('DD MM YYYY'); ?>";
});Usage: @datetime($date)
Inertia (React / Vue / Svelte) โ
Connects Leaf backend to a JS frontend framework without building a full API.
Setup (MVC) โ
leaf view:install --vue
leaf view:install --react
leaf view:install --svelteWhen you run view:install, Leaf automatically generates app/views/_inertia.blade.php โ the root HTML shell for all Inertia pages. Do not create this file manually.
{{-- app/views/_inertia.blade.php (auto-generated) --}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title inertia>{{ _env('APP_NAME', 'Leaf MVC') }}</title>
@viteReactRefresh
@vite(['/js/app.jsx', "/js/pages/{$page['component']}.jsx"])
@inertiaHead
</head>
<body>
@inertia
</body>
</html>This is the right place for global head content โ fonts, analytics (GTM, Tawk.to, OneSignal), favicon, CDN scripts, etc. Edit it when you need those things; otherwise leave it alone.
{{-- _inertia.blade.php with global assets --}}
<head>
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet">
@viteReactRefresh
@vite(['/js/app.jsx', "/js/pages/{$page['component']}.jsx"])
@inertiaHead
{{-- GTM, analytics, chat widgets go here --}}
</head>
<body>
@inertia
</body>Key directives:
@viteReactRefreshโ React HMR in dev (must come before@vite)@vite([...])โ loads compiled assets@inertiaHeadโ renders<Head>tags from React components@inertiaโ mounts the React/Vue/Svelte app
Returning Inertia Views โ
// From controller
response()->inertia('home', ['user' => auth()->user()]);
// Direct route
app()->inertia('/home', 'home');Naming: page files are kebab-case in lowercase folders (pages/order-history.jsx), and the string you pass to response()->inertia() matches the file name exactly. The component inside the file keeps React's PascalCase convention. Prefer response()->inertia() over the bare inertia() helper โ it returns a response like every other handler.
Generating View Files โ
leaf g:template home # auto-detects framework
leaf g:template home --type=jsx # React
leaf g:template home --type=vue
leaf g:template home --type=svelteShared Data (across all views) โ
// app/routes/index.php
use Leaf\Inertia;
Inertia::share('appName', 'My App');
Inertia::share('flash', function () {
return flash()->display('flash') ?? null;
});Leaf automatically shares an auth prop with every Inertia page: {id, user, roles, permissions, errors}. Do not share your own auth key โ the framework's value takes precedence and yours never renders. auth.user contains every column not listed in auth's hidden config.
React Component Example โ
export default function Home({ user, appName }) {
return <h1>Welcome to {appName}, {user.name}</h1>;
}Form Validation with Inertia โ
Controller:
$data = request()->validate(['email' => 'email']);
if (!$data) {
return response()
->withFlash('errors', request()->errors())
->redirect('/form', 303); // 303 is important for Inertia
}View (pass errors as prop):
response()->inertia('form', [
'errors' => flash()->display('errors') ?? [],
]);Frontend (React):
const { data, setData, patch, errors } = useForm({ email: '' });
// errors.email auto-populated from the errors propshadcn/ui (React) โ
leaf scaffold:shadcn
pnpm dlx shadcn@latest add buttonLite Apps โ
Everything above works in lite apps too, with a few differences in layout:
leaf view:install --react(or--vue/--svelte) works in lite apps and writes frontend files toviews/js/in the project root, notapp/views/โ that path belongs to the MVC layout.- Lite apps must configure the view paths before rendering. Newly scaffolded apps have this wired automatically as of CLI v5.0.6; older apps need it set by hand:
app()->config('views.path', 'views');
app()->config('views.cache', __DIR__ . '/storage/cache');- Vite serves from the project root in lite apps: the
hotfile and thebuild/directory live at the root, which matches leafs/vite's defaults (5.x latest). MVC apps usepublic/instead, wired automatically. g:templateandscaffold:*commands are MVC-only. They don't exist in a lite app's console, so create page files by hand.
Vite (Asset Bundling) โ
Pre-installed in MVC. For Basic apps:
leaf view:install --viteLoading Assets โ
@vite('css/app.css')
@vite(['app.css', 'app.js'])PHP (non-Blade):
<?php vite('css/app.css'); ?>vite.config.js โ
import { defineConfig } from 'vite';
import leaf from '@leafphp/vite-plugin';
export default defineConfig({
plugins: [
leaf({
input: ['path/to/app.css', 'path/to/app.js'],
refresh: true,
}),
],
resolve: {
alias: { '@': '/path/to/folder' },
},
});PHP Config (optional, non-MVC) โ
\Leaf\Vite::config([
'assets' => 'app/views',
'build' => 'public/build',
]);Vite dev server starts automatically with leaf serve.
Tailwind CSS โ
leaf view:install --tailwindInstalls Tailwind v4, updates vite.config.js, and sets up css/app.css.
Include in Blade layout:
@vite('css/app.css')Theming (Tailwind v4) โ
@theme {
--color-primary: #ff0000;
--color-secondary: #00ff00;
}<div class="bg-primary text-secondary">Hello</div>Custom / Third-Party Template Engines โ
Basic App โ
app()->attachView(Smarty::class);
app()->smarty()->setTemplateDir('/views');
app()->smarty()->assign('name', 'Michael');
app()->smarty()->display('index.tpl');MVC โ config/view.php โ
leaf config:publish view # โ config/view.phpreturn [
'viewEngine' => \Smarty::class,
'config' => function (\Smarty $engine, array $config) {
$engine->setTemplateDir($config['views']);
$engine->setCacheDir($config['cache']);
},
'render' => function (\Smarty $engine, $view, $data) {
foreach ($data as $key => $value) {
$engine->assign($key, $value);
}
$engine->display($view);
},
'extend' => null,
];