For more detailed docs take a look at the PenguBlade wiki as the components are the same.
Theming
NativeBlade is driven entirely by CSS custom properties (--nb-*). Three independent axes are switched
with classes on the <html> element:
1. Design language
- Material Design 3 (default) — add nothing, or set
class="ui-material". - iOS — set
class="ui-ios".
2. Color mode (light / dark)
Without JavaScript the light tokens act as the fallback. To support explicit color modes (and to follow the system preference live) apply one of:
class="nb-light"— force lightclass="nb-dark"— force dark
A small inline script in <head> resolves "auto" (persisted in localStorage under
nativeblade-color) to the current system preference and keeps it in sync:
<script>
(function () {
var root = document.documentElement;
try {
var color = localStorage.getItem('nativeblade-color') || 'auto';
if (color !== 'light' && color !== 'dark') {
color = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
root.classList.remove('nb-light', 'nb-dark');
root.classList.add('nb-' + color);
} catch (e) {
// localStorage unavailable — keep the default
}
})();
</script>
3. Brand palettes (Material "dynamic color" style)
Each palette overrides the brand tokens (primary, on-primary, primary-container, secondary, ...) and — in Material mode — the tonal surface and outline neutrals, so the whole screen (body background, cards, modal sheets, inputs) picks up the hue the way Material's dynamic color does. In iOS mode the system grays stay untouched and only the accent changes. Available palettes:
nb-palette-blue,nb-palette-green,nb-palette-orange,nb-palette-red- (the default M3 purple palette needs no class)
For fully custom palettes — e.g. derived from your logo or background — just override the individual tokens:
:root {
--nb-primary: #0B57D0;
--nb-on-primary: #FFFFFF;
--nb-primary-container: #D3E0FD;
--nb-on-primary-container: #041E49;
}
Every visual aspect (surfaces, text, lines, semantics, radii, shadows, font, elevation) is exposed as a
--nb-* token and can be overridden the same way.
Example
<div x-data="{
ui: document.documentElement.classList.contains('ui-ios') ? 'ios' : 'material',
setUi(next) {
this.ui = next;
const root = document.documentElement;
root.classList.remove('ui-material', 'ui-ios');
root.classList.add('ui-' + next);
try {
localStorage.setItem('nativeblade-ui', next);
} catch (e) {
// ignore
}
},
color: document.documentElement.classList.contains('nb-dark') ? 'dark'
: (document.documentElement.classList.contains('nb-light') ? 'light' : 'auto'),
setColor(next) {
this.color = next;
const root = document.documentElement;
root.classList.remove('nb-light', 'nb-dark');
const resolved = next === 'auto'
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: next;
root.classList.add('nb-' + resolved);
try {
localStorage.setItem('nativeblade-color', next);
} catch (e) {
// ignore
}
},
palette: (() => {
const match = document.documentElement.className.match(/nb-palette-(\w+)/);
return match ? match[1] : 'purple';
})(),
palettes: [
{ id: 'purple', name: 'Material (Default)', swatch: '#6750A4' },
{ id: 'blue', name: 'Blue', swatch: '#0B57D0' },
{ id: 'green', name: 'Green', swatch: '#4A7A1E' },
{ id: 'orange', name: 'Orange', swatch: '#B4490E' },
{ id: 'red', name: 'Red', swatch: '#BA1A1A' }
],
setPalette(next) {
this.palette = next;
const root = document.documentElement;
this.palettes.forEach((p) => root.classList.remove('nb-palette-' + p.id));
if (next !== 'purple') {
root.classList.add('nb-palette-' + next);
}
try {
localStorage.setItem('nativeblade-palette', next);
} catch (e) {
// ignore
}
}
}"
class="min-h-screen bg-surface text-on-surface">
{{-- Header with theme switcher --}}
<header class="sticky top-0 z-20 border-b border-outline/60 bg-surface/85 backdrop-blur">
<div class="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-x-4 gap-y-2 px-4 py-3 sm:px-6">
<div class="flex items-center gap-3">
<div class="flex size-9 items-center justify-center rounded-full bg-primary font-bold text-on-primary">
NB
</div>
<div>
<h1 class="text-base font-semibold leading-tight text-on-surface-strong">NativeBlade</h1>
<p class="hidden text-xs text-on-surface-muted sm:block">Native UI für Laravel & Livewire</p>
</div>
</div>
<div class="flex items-center gap-1 rounded-full bg-surface-alt p-1" role="group" aria-label="UI Theme">
<button type="button" @click="setUi('material')"
:class="ui === 'material' ? 'bg-surface text-on-surface-strong shadow-1' : 'text-on-surface-muted'"
class="cursor-pointer rounded-full px-3.5 py-1.5 text-xs font-semibold transition sm:px-4">
Material 3
</button>
<button type="button" @click="setUi('ios')"
:class="ui === 'ios' ? 'bg-surface text-on-surface-strong shadow-1' : 'text-on-surface-muted'"
class="cursor-pointer rounded-full px-3.5 py-1.5 text-xs font-semibold transition sm:px-4">
iOS
</button>
</div>
</div>
</header>
<main class="mx-auto max-w-6xl space-y-6 px-4 py-8 sm:px-6">
{{-- Intro --}}
<x-card>
<x-card.title :divider="false">NativeBlade</x-card.title>
<p class="mt-2 max-w-3xl text-sm text-on-surface">
Blade-Komponenten mit dem Look & feel nativer Apps — wählbar zwischen
<span class="font-semibold text-on-surface-strong">Material Design 3</span> und
<span class="font-semibold text-on-surface-strong">iOS</span>. Der Theme-Wechsler oben rechts setzt
einfach eine CSS-Klasse am <x-kbd><html></x-kbd>-Element, alle Farben, Radien, Schatten und
Fonts folgen über Design-Tokens. Light/Dark folgt dem System oder wird unten manuell gewählt —
dazu gibt es M3-artige <span class="font-semibold text-on-surface-strong">Farbpaletten</span>.
</p>
<div class="mt-4 flex flex-wrap items-center gap-2">
<x-badge type="soft" color="primary">Material 3</x-badge>
<x-badge type="soft" color="info">iOS</x-badge>
<x-badge type="soft" color="success">Livewire 4</x-badge>
<x-badge type="soft" color="warning">Alpine.js</x-badge>
</div>
</x-card>
{{-- Theme & Dynamic Color --}}
<x-card>
<x-card.title :divider="false">Theme & Dynamic Color</x-card.title>
<p class="mt-1 text-sm text-on-surface-muted">
Drei Achsen, alles über CSS-Klassen am <x-kbd><html></x-kbd>-Element: Design-Sprache
(<x-kbd>ui-material</x-kbd>/<x-kbd>ui-ios</x-kbd>), Farbmodus
(<x-kbd>nb-light</x-kbd>/<x-kbd>nb-dark</x-kbd>) und Pallette
(<x-kbd>nb-palette-*</x-kbd>). Jede Farbe ist ein <x-kbd>--nb-*</x-kbd>-Token und kann individuell
überschrieben werden.
</p>
<div class="mt-4 grid gap-4 sm:grid-cols-2">
<div class="flex flex-col gap-1.5">
<span class="pl-0.5 text-sm font-medium text-on-surface-strong">Farbmodus</span>
<div class="flex w-fit flex-wrap items-center gap-1 rounded-full bg-surface-alt p-1" role="group" aria-label="Farbmodus">
<button type="button" @click="setColor('auto')"
:class="color === 'auto' ? 'bg-surface text-on-surface-strong shadow-1' : 'text-on-surface-muted'"
class="flex cursor-pointer items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-semibold transition">
<i class="icon-auto size-3.5" aria-hidden="true"></i>Auto
</button>
<button type="button" @click="setColor('light')"
:class="color === 'light' ? 'bg-surface text-on-surface-strong shadow-1' : 'text-on-surface-muted'"
class="flex cursor-pointer items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-semibold transition">
<i class="icon-sun size-3.5" aria-hidden="true"></i>Hell
</button>
<button type="button" @click="setColor('dark')"
:class="color === 'dark' ? 'bg-surface text-on-surface-strong shadow-1' : 'text-on-surface-muted'"
class="flex cursor-pointer items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-semibold transition">
<i class="icon-moon size-3.5" aria-hidden="true"></i>Dunkel
</button>
</div>
</div>
<div class="flex flex-col gap-1.5">
<span class="pl-0.5 text-sm font-medium text-on-surface-strong">Pallette (Material Dynamic Color)</span>
<div class="flex flex-wrap items-center gap-2" role="group" aria-label="Farbpallette">
<template x-for="p in palettes" :key="p.id">
<button type="button" @click="setPalette(p.id)"
class="flex cursor-pointer items-center gap-1.5 rounded-full border py-1 pl-1.5 pr-3 text-xs font-semibold transition"
:class="palette === p.id ? 'border-primary bg-primary-container/50 text-on-surface-strong' : 'border-outline/60 text-on-surface-muted hover:border-outline'"
:aria-pressed="palette === p.id">
<span class="block size-4 rounded-full" :style="'background-color: ' + p.swatch"></span>
<span x-text="p.name"></span>
</button>
</template>
</div>
</div>
</div>
</x-card>
{{-- Buttons --}}
<x-card>
<x-card.title :divider="false">Buttons</x-card.title>
<p class="mt-1 text-sm text-on-surface-muted">
Material: Pill-Form mit State-Layers · iOS: flache Rechtecke mit Press-Feedback.
</p>
<div class="mt-4 flex flex-wrap items-center gap-3">
<div class="w-36"><x-button>Primary</x-button></div>
<div class="w-36"><x-button variant="outline">Outline</x-button></div>
<div class="w-36"><x-button color="danger">Danger</x-button></div>
<div class="w-36"><x-button color="success" variant="outline">Success</x-button></div>
<div class="w-36"><x-button color="warning" size="sm">Warning</x-button></div>
<div class="w-36"><x-button color="secondary" size="lg">Secondary</x-button></div>
</div>
<div class="mt-6 flex flex-wrap items-center gap-4">
<x-button.floating icon="icon-plus" tooltip="Floating Action Button"/>
<x-button.floating color="danger" icon="icon-minus" size="sm"/>
<x-button.floating color="inverse" icon="icon-check" size="lg"/>
<x-link>Textlink / Navigation →</x-link>
</div>
</x-card>
{{-- Profile form (wire:model + validation + file upload) --}}
<x-card>
<x-card.title :divider="false">Profil bearbeiten</x-card.title>
<p class="mt-1 text-sm text-on-surface-muted">
Live-gebunden via <x-kbd>wire:model</x-kbd> — inkl. Validierung, File Upload und Loading-State.
</p>
<form wire:submit="saveProfile" class="mt-4 flex flex-col gap-4">
<div class="grid gap-4 sm:grid-cols-2">
<x-input label="Name" wire:model="name" required hint="Mindestens 3 Zeichen."/>
<x-input label="E-Mail" type="email" wire:model="email" required placeholder="lena@example.com"/>
</div>
<x-select label="Plan" wire:model="plan" icon="icon-chevron-down">
<option value="basic">Basic</option>
<option value="pro">Pro</option>
<option value="business">Business</option>
</x-select>
<x-textarea label="Bio" wire:model="bio" rows="3" placeholder="Ein paar Worte über dich…"/>
<x-password label="Passwort" wire:model="password" show-generate show-toggle hint="Generator & Toggle mit Alpine.js."/>
<div class="grid gap-4 sm:grid-cols-2">
<x-checkbox label="Newsletter" wire:model="newsletter"/>
<x-toggle label="Push-Benachrichtigungen" wire:model="notifications"/>
</div>
<div class="flex flex-wrap gap-x-10 gap-y-4">
<x-toggle label="Wartungsmodus (success)" color="success" wire:model="maintenance"/>
<x-toggle label="Sperren (danger)" color="danger" wire:model="locked"/>
</div>
<div class="flex flex-col gap-2">
<span class="pl-0.5 text-sm font-medium text-on-surface-strong">Benachrichtigungskanal</span>
<div class="flex flex-col gap-2">
<x-radio label="E-Mail" wire:model="notificationChannel" value="email"/>
<x-radio label="Push" wire:model="notificationChannel" value="push"/>
<x-radio label="SMS" wire:model="notificationChannel" value="sms"/>
</div>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<x-counter label="Menge" wire:model="quantity" min="0" max="10"/>
<x-range label="Lautstärke" wire:model="volume" current="{{ $volume }}" show-value/>
</div>
<x-select.multiple label="Rollen" wire:model="roles">
<option value="admin">Admin</option>
<option value="editor">Editor</option>
<option value="viewer">Viewer</option>
<option value="billing" disabled>Abrechnung (gesperrt)</option>
</x-select.multiple>
<x-file label="Avatar" wire:model="file" max-size="1024" show-progress
hint="Livewire File Uploads mit Progress — max. 1 MB."/>
@if($file)
<div class="flex items-center gap-2 text-sm text-on-surface-muted">
<i class="icon-check size-4 text-success"></i>
<span wire:loading.remove wire:target="file">
Hochgeladen: <span class="font-medium text-on-surface-strong">{{ $file->getClientOriginalName() }}</span>
</span>
<span wire:loading wire:target="file">
{{ $file->getClientOriginalName() }} wird hochgeladen…
</span>
</div>
@endif
<x-button type="submit" loading="saveProfile">
Profil speichern
</x-button>
<div x-show="$wire.profileSaved" x-cloak>
<x-alert
type="success"
title="Gespeichert!"
message="Deine Änderungen wurden erfolgreich übernommen."
icon="icon-success"
dismissible/>
</div>
</form>
</x-card>
{{-- Tabs & accordion (wire:model) --}}
<x-card>
<x-card.title :divider="false">Tabs, Accordion & Modal</x-card.title>
<p class="mt-1 text-sm text-on-surface-muted">
Zustand via <x-kbd>wire:model</x-kbd> in Livewire synchronisiert.
</p>
<div class="mt-4">
<x-tab wire:model="activeTab">
<x-tab.item uuid="overview">Übersicht</x-tab.item>
<x-tab.item uuid="details">Details</x-tab.item>
<x-tab.item uuid="team">Team</x-tab.item>
</x-tab>
<div class="pt-4 text-sm text-on-surface" x-show="$wire.activeTab === 'overview'" x-cloak>
Alles auf einen Blick — mit <span class="font-semibold text-on-surface-strong">wire:model</span> bleibt der
ausgewählte Tab auch nach Livewire-Requests erhalten.
</div>
<div class="pt-4 text-sm text-on-surface" x-show="$wire.activeTab === 'details'" x-cloak>
Detaillierte Ansicht.
</div>
<div class="pt-4 text-sm text-on-surface" x-show="$wire.activeTab === 'team'" x-cloak>
Dein Team.
</div>
</div>
<div class="mt-6 grid gap-6 md:grid-cols-2">
<x-accordion wire:model="openAccordion">
<x-accordion.item uuid="faq-1">
<x-accordion.item.trigger>Wie wird das Theme gewechselt?</x-accordion.item.trigger>
<x-accordion.item.content>
Über die CSS-Klasse <x-kbd>ui-material</x-kbd> bzw. <x-kbd>ui-ios</x-kbd> am
<x-kbd><html></x-kbd>-Element — probier es oben rechts aus.
</x-accordion.item.content>
</x-accordion.item>
<x-accordion.item uuid="faq-2">
<x-accordion.item.trigger>Funktioniert das mit Livewire?</x-accordion.item.trigger>
<x-accordion.item.content>
Ja — alle interaktiven Komponenten laufen auf Alpine.js und harmonieren mit
<x-kbd>wire:model</x-kbd>, <x-kbd>wire:loading</x-kbd> und File Uploads.
</x-accordion.item.content>
</x-accordion.item>
<x-accordion.item uuid="faq-3">
<x-accordion.item.trigger>Was ist mit Dark Mode?</x-accordion.item.trigger>
<x-accordion.item.content>
Beide Themes folgen automatisch dem Systemschema
(<x-kbd>prefers-color-scheme</x-kbd>).
</x-accordion.item.content>
</x-accordion.item>
</x-accordion>
<div class="flex flex-col gap-4">
<x-modal wire:model="demoModalOpen">
<x-modal.content>
<x-modal.header>Bestätigung</x-modal.header>
<p class="text-sm text-on-surface">
Willst du wirklich alle Änderungen verwerfen? Das kann nicht rückgängig gemacht werden.
</p>
<x-modal.footer>
<x-button variant="outline" wire:click="closeModal">Abbrechen</x-button>
<x-button color="danger" wire:click="closeModal">Verwerfen</x-button>
</x-modal.footer>
</x-modal.content>
</x-modal>
<div class="max-w-56">
<x-button wire:click="openModal">
Modal öffnen
</x-button>
</div>
<div class="flex items-center gap-3">
<x-spinner/>
<span class="text-sm text-on-surface-muted">Lädt…</span>
<x-spinner color="success" size="sm"/>
<x-spinner color="danger" size="lg"/>
</div>
<x-progress label="Upload" :min="0" :max="100">
<x-progress.value :value="72"/>
</x-progress>
<x-progress label="Speicher" :min="0" :max="500" hint="287 von 500 GB belegt">
<x-progress.value :value="287" color="warning" show-text/>
</x-progress>
<x-skeleton type="text"/>
</div>
</div>
</x-card>
{{-- Badges, avatars, alerts --}}
<x-card>
<x-card.title :divider="false">Badges, Avatare & Alerts</x-card.title>
<div class="mt-4 flex flex-wrap items-center gap-2">
<x-badge>New</x-badge>
<x-badge color="primary">Primary</x-badge>
<x-badge color="success" type="soft">Aktiv</x-badge>
<x-badge color="warning" type="soft">Beta</x-badge>
<x-badge color="danger" type="soft">Fehler</x-badge>
<x-badge size="lg" color="info" type="soft">Info</x-badge>
<x-kbd>⌘</x-kbd>
<x-kbd>Shift</x-kbd>
<x-kbd>K</x-kbd>
</div>
<div class="mt-4 flex flex-wrap items-center gap-3">
<x-avatar placeholder-initials="LM" placeholder-color="primary" status="success"/>
<x-avatar placeholder-initials="MK" placeholder-color="info" status="warning"/>
<x-avatar placeholder-icon="icon-user" placeholder-color="secondary"/>
<x-avatar placeholder-initials="AB" placeholder-color="danger" size="lg"/>
</div>
<div class="mt-4 flex flex-col gap-3">
<x-alert type="info" title="Neu: iOS-Theme" message="Über die CSS-Klasse ui-ios aktivieren." icon="icon-info" dismissible/>
<x-alert type="warning" title="Achtung" message="Dein Abo läuft in 3 Tagen ab." icon="icon-warning" dismissible/>
<x-alert type="error" message="Verbindung zum Server verloren." icon="icon-error"/>
</div>
<div class="mt-6 border-t border-outline/60 pt-4">
<div class="flex flex-wrap items-center justify-between gap-4">
<x-breadcrumb>
<x-breadcrumb.item>Start</x-breadcrumb.item>
<x-breadcrumb.item>Komponenten</x-breadcrumb.item>
<x-breadcrumb.item last>Demo</x-breadcrumb.item>
</x-breadcrumb>
<div class="flex items-center gap-3">
<span class="text-sm text-on-surface-muted">Token kopieren:</span>
<x-copy text="sk_live_nativeblade_2026"/>
</div>
</div>
<div class="mt-4">
<x-steps>
<x-steps.step :completed="true" :number="1" label="Account"/>
<x-steps.step :current="true" :number="2" label="Verifizierung"/>
<x-steps.step :number="3" label="Fertig"/>
</x-steps>
</div>
</div>
</x-card>
{{-- Table --}}
<x-card>
<x-card.title :divider="false">Tabelle</x-card.title>
<div class="mt-4">
<x-table>
<x-table.header>
<x-table.header.item>Projekt</x-table.header.item>
<x-table.header.item>Status</x-table.header.item>
<x-table.header.item>Team</x-table.header.item>
<x-table.header.item>Progress</x-table.header.item>
</x-table.header>
<x-table.body>
<x-table.body.row>
<x-table.body.item class="font-medium text-on-surface-strong">Mobile App</x-table.body.item>
<x-table.body.item><x-badge color="success" type="soft">In Arbeit</x-badge></x-table.body.item>
<x-table.body.item>
<div class="flex items-center gap-2">
<x-avatar placeholder-initials="LM" placeholder-color="primary" size="sm"/>
<span>Lena M.</span>
</div>
</x-table.body.item>
<x-table.body.item>
<div class="w-40">
<x-progress><x-progress.value :value="64"/></x-progress>
</div>
</x-table.body.item>
</x-table.body.row>
<x-table.body.row>
<x-table.body.item class="font-medium text-on-surface-strong">Website Relaunch</x-table.body.item>
<x-table.body.item><x-badge color="warning" type="soft">Review</x-badge></x-table.body.item>
<x-table.body.item>
<div class="flex items-center gap-2">
<x-avatar placeholder-initials="MK" placeholder-color="info" size="sm"/>
<span>Max K.</span>
</div>
</x-table.body.item>
<x-table.body.item>
<div class="w-40">
<x-progress><x-progress.value :value="38" color="warning"/></x-progress>
</div>
</x-table.body.item>
</x-table.body.row>
<x-table.body.row>
<x-table.body.item class="font-medium text-on-surface-strong">API v2</x-table.body.item>
<x-table.body.item><x-badge color="info" type="soft">Geplant</x-badge></x-table.body.item>
<x-table.body.item>
<div class="flex items-center gap-2">
<x-avatar placeholder-initials="JS" placeholder-color="secondary" size="sm"/>
<span>Jule S.</span>
</div>
</x-table.body.item>
<x-table.body.item>
<div class="w-40">
<x-progress><x-progress.value :value="12" color="info"/></x-progress>
</div>
</x-table.body.item>
</x-table.body.row>
</x-table.body>
</x-table>
</div>
</x-card>
</main>
<footer class="border-t border-outline/60 py-6">
<p class="text-center text-xs text-on-surface-muted">
NativeBlade — Material Design 3 & iOS · Laravel Livewire + Alpine.js
</p>
</footer>
</div>