211 lines
9.3 KiB
Markdown
211 lines
9.3 KiB
Markdown
# 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 }">`:
|
|
|
|
```html
|
|
<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:
|
|
|
|
```html
|
|
<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:
|
|
|
|
```html
|
|
<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:
|
|
|
|
```html
|
|
<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:
|
|
|
|
```html
|
|
<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>
|
|
```
|