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.
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.
<!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>
| Prop | Values |
|---|---|
title | Tab title; defaults to config('app.name') |
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).
<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>
| Prop | Values |
|---|---|
theme | light | dark | system |
brand-color | default | green | red | yellow | blue | gray | dark | #rrggbb |
sidebar-mode | expanded | collapsed |
navbar-mode | fixed | static | hidden |
skin | default | bordered |
semi-dark | true | false |
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.
// 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();
}
| Prop | Values |
|---|---|
preference-url | POST URL; when null, the preferences are stored only in the browser |
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.
<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>
| Prop | Values |
|---|---|
$store.appearance | value · set(light | dark | system) · toggle() |
$store.brand | color · set(name | #rrggbb) |
$store.sidebar | collapsed · toggle() · setCollapsed(true | false) |
$store.navbar | mode · set(fixed | static | hidden) |
$store.customizer | skin · semiDark · setSkin() · setSemiDark() · reset() |
Props and slots
The values the component accepts.
| Prop | Default | Description |
|---|---|---|
title | null | Tab title; defaults to config('app.name') |
theme | null | light · dark · system |
brand-color | null | default · green · red · yellow · blue · gray · dark · #rrggbb |
sidebar-mode | null | expanded · collapsed |
navbar-mode | null | fixed · static · hidden; when null, the preference in the browser, or fixed if there is none |
skin | null | default · bordered |
semi-dark | null | true · false; when null, the preference in the browser |
preference-url | null | URL that the preferences are POSTed to; when null, they are stored only in the browser |