9.3 KiB
9.3 KiB
Alpine.js Core Patterns for Single-Page Applications ⚡
These zero-build Alpine.js patterns provide reactive state management directly in HTML. Use these snippets to make your single-page websites snappy, interactive, and responsive.
1. Dark/Light Mode Switcher with Persistence
Place this in <html x-data="{ darkMode: localStorage.getItem('theme') === 'dark' }" :class="{ 'dark': darkMode }">:
<button
@click="darkMode = !darkMode; localStorage.setItem('theme', darkMode ? 'dark' : 'light')"
class="p-2.5 rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all cursor-pointer"
title="Toggle Theme">
<i x-show="!darkMode" data-lucide="moon" class="w-5 h-5"></i>
<i x-show="darkMode" data-lucide="sun" class="w-5 h-5"></i>
</button>
2. Dynamic Tab Navigation
Switch between multiple views without page reloads:
<div x-data="{ activeTab: 'overview' }" class="w-full">
<!-- Tab Bar -->
<div class="flex gap-2 p-1.5 bg-zinc-100 dark:bg-zinc-900 rounded-xl max-w-md mx-auto mb-8 border border-zinc-200 dark:border-zinc-800">
<button
@click="activeTab = 'overview'"
:class="activeTab === 'overview' ? 'bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white shadow-sm' : 'text-zinc-500 hover:text-zinc-900 dark:hover:text-white'"
class="flex-1 py-2 px-4 rounded-lg font-medium text-sm transition-all flex items-center justify-center gap-2">
<i data-lucide="layout-grid" class="w-4 h-4"></i> Overview
</button>
<button
@click="activeTab = 'calculator'"
:class="activeTab === 'calculator' ? 'bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white shadow-sm' : 'text-zinc-500 hover:text-zinc-900 dark:hover:text-white'"
class="flex-1 py-2 px-4 rounded-lg font-medium text-sm transition-all flex items-center justify-center gap-2">
<i data-lucide="calculator" class="w-4 h-4"></i> Calculator
</button>
</div>
<!-- Tab Contents with Transitions -->
<div x-show="activeTab === 'overview'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-y-2" x-transition:enter-end="opacity-100 translate-y-0">
<!-- Overview View Content -->
</div>
<div x-show="activeTab === 'calculator'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-y-2" x-transition:enter-end="opacity-100 translate-y-0">
<!-- Calculator View Content -->
</div>
</div>
3. Instant Search & Category Filter
Real-time filtering of cards or list items:
<div x-data="{
search: '',
activeCategory: 'all',
items: [
{ title: 'Pistazien Gelato', category: 'creami', protein: '28g', calories: 340, desc: 'Reichhaltiges Gelato mit echter sizilianischer Pistazienpaste.' },
{ title: 'Protein Erdbeer Sorbet', category: 'protein', protein: '35g', calories: 210, desc: 'Frisches Erdbeersorbet mit Clear Whey Isolat.' },
{ title: 'Mango Maracuja Slush', category: 'fruit', protein: '2g', calories: 140, desc: 'Perfekte Sommererfrischung mit Mango-Chunks.' }
],
get filteredItems() {
return this.items.filter(item => {
const matchesSearch = item.title.toLowerCase().includes(this.search.toLowerCase()) || item.desc.toLowerCase().includes(this.search.toLowerCase());
const matchesCategory = this.activeCategory === 'all' || item.category === this.activeCategory;
return matchesSearch && matchesCategory;
});
}
}">
<!-- Search & Pills -->
<div class="flex flex-col sm:flex-row gap-4 mb-8">
<div class="relative flex-1">
<i data-lucide="search" class="w-4 h-4 absolute left-3.5 top-3.5 text-zinc-400"></i>
<input
x-model="search"
type="text"
placeholder="Rezepte durchsuchen..."
class="w-full pl-10 pr-4 py-2.5 rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-cyan-500 text-sm">
</div>
<div class="flex gap-2 overflow-x-auto pb-1">
<template x-for="cat in ['all', 'creami', 'protein', 'fruit']" :key="cat">
<button
@click="activeCategory = cat"
:class="activeCategory === cat ? 'bg-cyan-500 text-white font-semibold' : 'bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 hover:bg-zinc-200'"
class="px-4 py-2 rounded-xl text-xs uppercase tracking-wider capitalize whitespace-nowrap transition-all cursor-pointer"
x-text="cat === 'all' ? 'Alle Rezepte' : cat">
</button>
</template>
</div>
</div>
<!-- Cards Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<template x-for="item in filteredItems" :key="item.title">
<div class="p-6 rounded-2xl bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 hover:border-cyan-500/50 shadow-sm hover:shadow-xl transition-all">
<h3 class="text-lg font-bold text-zinc-900 dark:text-white mb-2" x-text="item.title"></h3>
<p class="text-sm text-zinc-500 dark:text-zinc-400 mb-4" x-text="item.desc"></p>
<div class="flex justify-between items-center text-xs font-semibold text-cyan-600 dark:text-cyan-400">
<span x-text="item.protein + ' Protein'"></span>
<span x-text="item.calories + ' kcal'"></span>
</div>
</div>
</template>
</div>
</div>
4. Live Reactive Slider & Calculator
Instant calculations with automatic UI updates:
<div x-data="{
pints: 1,
baseMilk: 300,
baseWhey: 35,
basePistachio: 30,
get milk() { return this.baseMilk * this.pints; },
get whey() { return this.baseWhey * this.pints; },
get pistachio() { return this.basePistachio * this.pints; },
get proteinTotal() { return Math.round(this.whey * 0.8 + (this.milk * 0.034)); },
celebrate() {
if (typeof confetti === 'function') {
confetti({ particleCount: 75, spread: 60, origin: { y: 0.8 } });
}
}
}" class="p-6 rounded-2xl bg-zinc-900 border border-white/10 text-white">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold font-heading">Portions-Rechner</h3>
<span class="px-3 py-1 bg-cyan-500/20 text-cyan-400 rounded-full text-xs font-semibold" x-text="pints + (pints === 1 ? ' Pint' : ' Pints')"></span>
</div>
<div class="mb-6">
<label class="block text-xs uppercase tracking-wider text-zinc-400 mb-2">Anzahl Ninja CREAMi Deluxe Pints (680ml)</label>
<input type="range" min="1" max="4" step="1" x-model.number="pints" class="w-full accent-cyan-400 cursor-pointer">
</div>
<!-- Computed Ingredients -->
<div class="space-y-3 mb-6">
<div class="flex justify-between py-2 border-b border-white/5 text-sm">
<span class="text-zinc-400">Milch 1.5% oder Hafermilch</span>
<span class="font-bold text-white" x-text="milk + ' ml'"></span>
</div>
<div class="flex justify-between py-2 border-b border-white/5 text-sm">
<span class="text-zinc-400">Proteinpulver (Whey/Casein)</span>
<span class="font-bold text-cyan-400" x-text="whey + ' g'"></span>
</div>
<div class="flex justify-between py-2 border-b border-white/5 text-sm">
<span class="text-zinc-400">Pistazienmus (100%)</span>
<span class="font-bold text-emerald-400" x-text="pistachio + ' g'"></span>
</div>
</div>
<button @click="celebrate()" class="w-full py-3 bg-gradient-to-r from-cyan-500 to-blue-600 rounded-xl font-bold text-sm hover:brightness-110 active:scale-95 transition-all">
Zutaten vorbereitet! 🎉
</button>
</div>
5. Modal Dialog with Escape & Backdrop Blur
Accessible modal overlay:
<div x-data="{ open: false }">
<button @click="open = true" class="px-4 py-2 bg-zinc-900 text-white rounded-xl text-sm font-medium">
Details öffnen
</button>
<div
x-show="open"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
style="display: none;">
<div
@click.outside="open = false"
class="bg-white dark:bg-zinc-900 rounded-2xl max-w-lg w-full p-6 border border-zinc-200 dark:border-zinc-800 shadow-2xl"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-zinc-900 dark:text-white">Modal Titel</h3>
<button @click="open = false" class="text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-6">
Hier befindet sich detaillierter Inhalt mit Reaktivität oder Formularen.
</p>
<div class="flex justify-end gap-3">
<button @click="open = false" class="px-4 py-2 text-sm text-zinc-500 hover:text-zinc-800 dark:hover:text-white">Schließen</button>
<button @click="open = false" class="px-4 py-2 text-sm font-medium bg-cyan-500 text-white rounded-xl">Bestätigen</button>
</div>
</div>
</div>
</div>