Alpine.js Complete Guide | Lightweight JavaScript Framework

Alpine.js Complete Guide | Lightweight JavaScript Framework

이 글의 핵심

Alpine.js is a lightweight JavaScript framework that offers reactive and declarative nature of Vue.js at a fraction of the cost. Perfect for adding interactivity to server-rendered HTML.

Introduction

Alpine.js is a rugged, minimal framework for composing JavaScript behavior in your markup. It offers the reactive and declarative nature of big frameworks like Vue or React at a much lower cost.

The Problem

Traditional approach with vanilla JavaScript:

<button id="toggle">Toggle</button>
<div id="content" style="display: none;">Content</div>

<script>
  const button = document.getElementById('toggle');
  const content = document.getElementById('content');
  let isOpen = false;
  
  button.addEventListener('click', () => {
    isOpen = !isOpen;
    content.style.display = isOpen ? 'block' : 'none';
  });
</script>

The Solution

With Alpine.js:

<div x-data="{ open: false }">
  <button @click="open = !open">Toggle</button>
  <div x-show="open">Content</div>
</div>

1. Installation

CDN (Quick Start)

<!DOCTYPE html>
<html>
<head>
  <script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
</head>
<body>
  <div x-data="{ count: 0 }">
    <button @click="count++">Increment</button>
    <span x-text="count"></span>
  </div>
</body>
</html>

NPM

npm install alpinejs
import Alpine from 'alpinejs';

window.Alpine = Alpine;
Alpine.start();

2. Core Directives

x-data (State)

<div x-data="{ name: 'John', age: 30 }">
  <p x-text="name"></p>
  <p x-text="age"></p>
</div>

x-show / x-if (Conditional)

<!-- x-show: toggles display CSS -->
<div x-data="{ open: false }">
  <button @click="open = !open">Toggle</button>
  <div x-show="open">I'm visible!</div>
</div>

<!-- x-if: adds/removes from DOM -->
<template x-if="user">
  <div x-text="user.name"></div>
</template>

x-for (Loop)

<div x-data="{ items: ['Apple', 'Banana', 'Orange'] }">
  <template x-for="item in items" :key="item">
    <li x-text="item"></li>
  </template>
</div>

x-on / @ (Events)

<div x-data="{ count: 0 }">
  <!-- Long form -->
  <button x-on:click="count++">Increment</button>
  
  <!-- Short form -->
  <button @click="count++">Increment</button>
  
  <!-- With modifiers -->
  <form @submit.prevent="handleSubmit">
    <input @keyup.enter="submit">
  </form>
</div>

x-bind / : (Attributes)

<div x-data="{ isActive: true, color: 'blue' }">
  <!-- Long form -->
  <div x-bind:class="{ active: isActive }"></div>
  
  <!-- Short form -->
  <div :class="{ active: isActive }"></div>
  
  <!-- Multiple attributes -->
  <div :class="isActive && 'active'" :style="`color: ${color}`"></div>
</div>

x-model (Two-way Binding)

<div x-data="{ search: '' }">
  <input type="text" x-model="search" placeholder="Search...">
  <p>Searching for: <span x-text="search"></span></p>
</div>

x-text / x-html

<div x-data="{ message: 'Hello World', html: '<strong>Bold</strong>' }">
  <!-- Text content -->
  <p x-text="message"></p>
  
  <!-- HTML content -->
  <div x-html="html"></div>
</div>

3. Practical Examples

<div x-data="{ open: false }" @click.away="open = false">
  <button @click="open = !open">Menu</button>
  
  <div x-show="open" x-transition>
    <a href="#">Profile</a>
    <a href="#">Settings</a>
    <a href="#">Logout</a>
  </div>
</div>

Tabs

<div x-data="{ tab: 'home' }">
  <nav>
    <button @click="tab = 'home'" :class="{ active: tab === 'home' }">
      Home
    </button>
    <button @click="tab = 'profile'" :class="{ active: tab === 'profile' }">
      Profile
    </button>
  </nav>
  
  <div x-show="tab === 'home'">Home content</div>
  <div x-show="tab === 'profile'">Profile content</div>
</div>
<div x-data="{ open: false }">
  <button @click="open = true">Open Modal</button>
  
  <div x-show="open" 
       x-transition
       @click.away="open = false"
       class="modal">
    <div class="modal-content">
      <h2>Modal Title</h2>
      <p>Modal content</p>
      <button @click="open = false">Close</button>
    </div>
  </div>
</div>

Search Filter

<div x-data="{
  search: '',
  items: ['Apple', 'Banana', 'Cherry', 'Date'],
  get filteredItems() {
    return this.items.filter(i => 
      i.toLowerCase().includes(this.search.toLowerCase())
    );
  }
}">
  <input type="text" x-model="search" placeholder="Search fruits...">
  
  <template x-for="item in filteredItems" :key="item">
    <li x-text="item"></li>
  </template>
</div>

4. Advanced Features

Alpine.data (Component)

<script>
  document.addEventListener('alpine:init', () => {
    Alpine.data('dropdown', () => ({
      open: false,
      toggle() {
        this.open = !this.open;
      }
    }));
  });
</script>

<div x-data="dropdown">
  <button @click="toggle">Toggle</button>
  <div x-show="open">Dropdown content</div>
</div>

$watch (Reactive)

<div x-data="{ count: 0 }" x-init="$watch('count', value => console.log('Count:', value))">
  <button @click="count++">Increment</button>
</div>

$refs (DOM Access)

<div x-data="{}">
  <input x-ref="input" type="text">
  <button @click="$refs.input.focus()">Focus Input</button>
</div>

5. Transitions

<div x-data="{ open: false }">
  <button @click="open = !open">Toggle</button>
  
  <!-- Simple transition -->
  <div x-show="open" x-transition>
    Fade in/out
  </div>
  
  <!-- Custom transition -->
  <div x-show="open"
       x-transition:enter="transition ease-out duration-300"
       x-transition:enter-start="opacity-0 transform scale-90"
       x-transition:enter-end="opacity-100 transform scale-100"
       x-transition:leave="transition ease-in duration-300"
       x-transition:leave-start="opacity-100 transform scale-100"
       x-transition:leave-end="opacity-0 transform scale-90">
    Custom animation
  </div>
</div>

6. Ajax Example

<div x-data="{
  users: [],
  loading: false,
  async fetchUsers() {
    this.loading = true;
    const res = await fetch('/api/users');
    this.users = await res.json();
    this.loading = false;
  }
}" x-init="fetchUsers()">
  <div x-show="loading">Loading...</div>
  
  <template x-for="user in users" :key="user.id">
    <div x-text="user.name"></div>
  </template>
</div>

7. Best Practices

1. Keep State Close

<!-- Good: state is scoped to component -->
<div x-data="{ count: 0 }">
  <button @click="count++">Increment</button>
  <span x-text="count"></span>
</div>

<!-- Bad: global state -->
<script>
  let globalCount = 0;
</script>

2. Use Alpine.data for Reusability

Alpine.data('counter', (initial = 0) => ({
  count: initial,
  increment() {
    this.count++;
  }
}));
<div x-data="counter(5)">
  <button @click="increment">Increment</button>
  <span x-text="count"></span>
</div>

3. Use x-cloak to Prevent Flash

[x-cloak] { display: none !important; }
<div x-data="{ show: false }" x-cloak>
  <!-- Won't flash before Alpine loads -->
</div>

8. Framework Comparison

FeatureAlpine.jsReactVue
Size15KB40KB34KB
Learning CurveEasyMediumMedium
Use CaseSprinklesSPASPA
Build RequiredNoYesOptional

Summary

Alpine.js is perfect for adding interactivity to server-rendered pages:

  • Lightweight - only 15KB
  • No build step - works with CDN
  • Declarative - Vue-like syntax
  • Progressive - enhance existing HTML
  • Simple - learn in minutes

Key Takeaways:

  1. Use x-data to define component state
  2. Use @click for event handling
  3. Use x-show/x-if for conditionals
  4. Use x-for for loops
  5. Perfect for dropdowns, modals, tabs

Next Steps:

  • Learn HTMX
  • Build with Astro
  • Compare with Vanilla JavaScript

Resources: