EN
Getting started
08 · Layout

Head

The <head> content: meta tags, the page title, the appearance settings (as data) and a small script (acun-ui-boot.js) that applies the theme and menu state before the first paint. The Alpine stores (appearance, brand, sidebar, navbar, customizer) come from the Acun UI JavaScript: import '@acunsoft/acun-ui-core' or @acunUiScripts. The ready-made layouts add it themselves; you only need it when writing your own layout.

acun:layout.head
01

In your own layout

Put it inside <head>; it writes the charset, viewport and csrf-token meta tags, the <title> tag and the @livewireStyles output itself. Add your assets such as the favicon and @vite to the slot. @livewireScripts stays with you at the end of <body>. Because the theme script runs before the page is painted, there is no theme flash on wire:navigate transitions either.

BLADE
<!DOCTYPE html>
<html lang="en">
<head>
    <acun:layout.head :title="$title ?? null">
        <link rel="icon" href="/favicon.svg" type="image/svg+xml">
        @vite(['resources/css/app.css', 'resources/js/app.js'])
    </acun:layout.head>
</head>
<body>
    {{ $slot }}

    @livewireScripts
</body>
</html>
PropValues
titleTab title; defaults to config('app.name')
02

Saved preferences

The same order applies to every preference: first the value given by the server; if that is null, the value stored in the browser (localStorage); if there is none, the application default in config/acun-ui.php; and failing that, the package default (fixed for navbar-mode). A single page can force its own value with Acun\Ui\Support\Layout::page(). Since null means "not saved", do not pass a separate default for users without a saved value. The values are written to the <html> tag in the first frame (the dark class, data-brand-color, data-skin, data-sidebar-collapsed).

BLADE
<acun:layout.head
    :theme="auth()->user()?->theme_preference"
    :brand-color="auth()->user()?->brand_color"
    :sidebar-mode="auth()->user()?->sidebar_mode"
    :navbar-mode="auth()->user()?->navbar_mode"
    :skin="auth()->user()?->skin"
    :semi-dark="auth()->user()?->semi_dark"
    :preference-url="route('ui-preference.update')"
>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</acun:layout.head>
PropValues
themelight | dark | system
brand-colordefault | green | red | yellow | blue | gray | dark | #rrggbb
sidebar-modeexpanded | collapsed
navbar-modefixed | static | hidden
skindefault | bordered
semi-darktrue | false
03

Saving preferences to the account

When preference-url is given, the new value is POSTed to this URL as JSON as soon as the user changes the look; the request carries the X-CSRF-TOKEN header. Each request contains only the key that changed: theme, brand, sidebar, navbar, skin or semiDark. The request runs in the background; even if it fails, the look is kept for that session.

PHP
// routes/web.php
Route::post('ui-preference', UiPreferenceController::class)
    ->middleware('auth')
    ->name('ui-preference.update');

// app/Http/Controllers/UiPreferenceController.php
public function __invoke(Request $request): Response
{
    $data = $request->validate([
        'theme' => ['sometimes', 'in:light,dark,system'],
        'brand' => ['sometimes', 'regex:/^(#[0-9a-fA-F]{6}|default|green|red|yellow|blue|gray|dark)$/'],
        'sidebar' => ['sometimes', 'in:expanded,collapsed'],
        'navbar' => ['sometimes', 'in:fixed,static,hidden'],
        'skin' => ['sometimes', 'in:default,bordered'],
        'semiDark' => ['sometimes', 'boolean'],
    ]);

    $columns = [
        'theme' => 'theme_preference', 'brand' => 'brand_color', 'sidebar' => 'sidebar_mode',
        'navbar' => 'navbar_mode', 'skin' => 'skin', 'semiDark' => 'semi_dark',
    ];

    $request->user()->update(
        collect($data)->mapWithKeys(fn ($value, $key) => [$columns[$key] => $value])->all()
    );

    return response()->noContent();
}
PropValues
preference-urlPOST URL; when null, the preferences are stored only in the browser
04

Using the stores

The stores are registered when Alpine starts and are reachable from anywhere through $store. The set and toggle methods apply the value right away, write it to the browser and, when preference-url is set, save it to the account. The buttons below work inside this example's own frame; since the primary color choice is stored in the browser, undo it with "Default color" when you are done.

LIVE EXAMPLE
<div x-data class="flex flex-wrap items-center gap-2">
    <acun:button variant="white" x-on:click="$store.appearance.toggle()">Change theme</acun:button>
    <acun:button variant="white" x-on:click="$store.brand.set('green')">Green primary color</acun:button>
    <acun:button variant="white" x-on:click="$store.brand.set('default')">Default color</acun:button>
    <span class="text-sm text-gray-500 dark:text-zinc-400">
        Theme: <b x-text="$store.appearance.value"></b> · Color: <b x-text="$store.brand.color"></b>
    </span>
</div>
PropValues
$store.appearancevalue · set(light | dark | system) · toggle()
$store.brandcolor · set(name | #rrggbb)
$store.sidebarcollapsed · toggle() · setCollapsed(true | false)
$store.navbarmode · set(fixed | static | hidden)
$store.customizerskin · semiDark · setSkin() · setSemiDark() · reset()
API

Props and slots

The values the component accepts.

PropDefaultDescription
titlenullTab title; defaults to config('app.name')
themenulllight · dark · system
brand-colornulldefault · green · red · yellow · blue · gray · dark · #rrggbb
sidebar-modenullexpanded · collapsed
navbar-modenullfixed · static · hidden; when null, the preference in the browser, or fixed if there is none
skinnulldefault · bordered
semi-darknulltrue · false; when null, the preference in the browser
preference-urlnullURL that the preferences are POSTed to; when null, they are stored only in the browser
Slots: default (the application's head assets: @vite, favicon, fonts)
Acun UI · HeadDetailed usage and examples