Files
mop-skills/skills/create-website/references/alpine-patterns.md
T

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>