home / skills / brettatoms / agent-skills / alpinejs

alpinejs skill

/alpinejs

This skill helps you write clean Alpine.js code by enforcing concise directives, extracting long logic, and using reusable patterns for state and events.

npx playbooks add skill brettatoms/agent-skills --skill alpinejs

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
6.0 KB
---
name: alpinejs
description: AlpineJS best practices and patterns. Use when writing HTML with Alpine.js directives to avoid common mistakes like long inline JavaScript strings.
---

# AlpineJS Best Practices

## Golden Rule: Keep Attributes Short

**Never** put complex logic in HTML attributes. If your `x-data`, `x-init`, or any directive exceeds ~50 characters, extract it.

## Directive Cheatsheet

| Directive | Purpose | Example |
|-----------|---------|---------|
| `x-data` | Declare reactive component state | `x-data="{ open: false }"` |
| `x-init` | Run code on component init | `x-init="fetchData()"` |
| `x-show` | Toggle visibility (CSS display) | `x-show="open"` |
| `x-if` | Conditional rendering (must wrap `<template>`) | `<template x-if="show">` |
| `x-for` | Loop (must wrap `<template>`) | `<template x-for="item in items">` |
| `x-bind:` / `:` | Bind attribute to expression | `:class="{ active: isActive }"` |
| `x-on:` / `@` | Listen to events | `@click="open = !open"` |
| `x-model` | Two-way bind form inputs | `x-model="email"` |
| `x-text` | Set text content | `x-text="message"` |
| `x-html` | Set inner HTML | `x-html="htmlContent"` |
| `x-ref` | Reference element via `$refs` | `x-ref="input"` |
| `x-cloak` | Hide until Alpine initializes | `x-cloak` (add CSS: `[x-cloak] { display: none; }`) |
| `x-transition` | Apply enter/leave transitions | `x-transition` or `x-transition.duration.300ms` |
| `x-effect` | Run reactive side effects | `x-effect="console.log(count)"` |
| `x-ignore` | Skip Alpine initialization | `x-ignore` |
| `x-teleport` | Move element to another location | `x-teleport="#modals"` |
| `x-modelable` | Expose property for external binding | `x-modelable="value"` |

## Magic Properties

| Property | Description |
|----------|-------------|
| `$el` | Current DOM element |
| `$refs` | Access elements with `x-ref` |
| `$store` | Access global Alpine stores |
| `$watch` | Watch a property for changes |
| `$dispatch` | Dispatch custom events |
| `$nextTick` | Run after DOM updates |
| `$root` | Root element of component |
| `$data` | Access component data object |
| `$id` | Generate unique IDs |

## Patterns

### ❌ BAD: Long Inline JavaScript

```html
<!-- DON'T DO THIS -->
<div x-data="{ items: [], loading: true, error: null, async fetchItems() { this.loading = true; try { const res = await fetch('/api/items'); this.items = await res.json(); } catch (e) { this.error = e.message; } finally { this.loading = false; } } }" x-init="fetchItems()">
```

### ✅ GOOD: Extract to Function

```html
<script>
function itemList() {
  return {
    items: [],
    loading: true,
    error: null,
    
    async fetchItems() {
      this.loading = true;
      try {
        const res = await fetch('/api/items');
        this.items = await res.json();
      } catch (e) {
        this.error = e.message;
      } finally {
        this.loading = false;
      }
    }
  };
}
</script>

<div x-data="itemList()" x-init="fetchItems()">
  <!-- template -->
</div>
```

### ✅ GOOD: Simple Inline State

```html
<!-- Simple state is fine inline -->
<div x-data="{ open: false, count: 0 }">
  <button @click="open = !open">Toggle</button>
  <div x-show="open" x-transition>Content</div>
</div>
```

### ✅ GOOD: Global Store for Shared State

```html
<script>
document.addEventListener('alpine:init', () => {
  Alpine.store('cart', {
    items: [],
    add(item) { this.items.push(item); },
    get total() { return this.items.reduce((sum, i) => sum + i.price, 0); }
  });
});
</script>

<div x-data>
  <span x-text="$store.cart.total"></span>
</div>
```

### ✅ GOOD: Reusable Component with Alpine.data()

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

<div x-data="dropdown" @click.outside="close()">
  <button @click="toggle()">Menu</button>
  <ul x-show="open" x-transition>
    <li>Item 1</li>
  </ul>
</div>
```

### ✅ GOOD: Form with Validation

```html
<script>
function contactForm() {
  return {
    email: '',
    message: '',
    errors: {},
    
    validate() {
      this.errors = {};
      if (!this.email.includes('@')) this.errors.email = 'Invalid email';
      if (this.message.length < 10) this.errors.message = 'Too short';
      return Object.keys(this.errors).length === 0;
    },
    
    submit() {
      if (this.validate()) {
        // submit logic
      }
    }
  };
}
</script>

<form x-data="contactForm()" @submit.prevent="submit()">
  <input x-model="email" type="email">
  <span x-show="errors.email" x-text="errors.email" class="error"></span>
  
  <textarea x-model="message"></textarea>
  <span x-show="errors.message" x-text="errors.message" class="error"></span>
  
  <button type="submit">Send</button>
</form>
```

### Event Modifiers

```html
@click.prevent     <!-- preventDefault() -->
@click.stop        <!-- stopPropagation() -->
@click.outside     <!-- Click outside element -->
@click.window      <!-- Listen on window -->
@click.document    <!-- Listen on document -->
@click.once        <!-- Fire once -->
@click.debounce    <!-- Debounce (default 250ms) -->
@click.throttle    <!-- Throttle -->
@keydown.enter     <!-- Specific key -->
@keydown.escape    <!-- Escape key -->
```

### Transition Modifiers

```html
x-transition                           <!-- Default fade -->
x-transition.duration.300ms            <!-- Custom duration -->
x-transition.opacity                   <!-- Opacity only -->
x-transition.scale.90                  <!-- Scale from 90% -->
x-transition:enter.duration.500ms      <!-- Enter duration -->
x-transition:leave.duration.200ms      <!-- Leave duration -->
```

## Quick Decision Guide

1. **State is 1-3 simple properties?** → Inline `x-data="{ open: false }"`
2. **Has methods or complex logic?** → Extract to `function componentName() { return {...} }`
3. **Reused across pages?** → Use `Alpine.data('name', () => ({...}))`
4. **Shared global state?** → Use `Alpine.store('name', {...})`
5. **Long attribute string?** → You're doing it wrong. Extract it.

Overview

This skill provides Alpine.js best practices and component patterns to keep HTML clean, maintainable, and performant. It emphasizes extracting complex logic from directives, using stores and Alpine.data for reusable behavior, and choosing the right directive for common UI needs. Use it to avoid long inline JavaScript and to standardize component design across projects.

How this skill works

The skill inspects common Alpine.js anti-patterns and recommends concise alternatives: extract complex x-data/x-init logic into functions or Alpine.data, prefer inline state only for 1–3 simple properties, and use Alpine.store for shared state. It outlines directive usage, magic properties, event and transition modifiers, and practical component and form patterns developers can copy. Examples show how to convert long attribute strings into maintainable scripts and reusable data providers.

When to use it

  • You find x-data, x-init, or other directives longer than ~50 characters.
  • You need reusable components or shared state across pages.
  • You see repeated inline logic across multiple templates.
  • You want predictable, testable form validation and async data fetching.
  • You need clear guidance on which directive or magic property to use.

Best practices

  • Keep attributes short: if a directive grows complex, extract it into a function or Alpine.data.
  • Inline state is fine for 1–3 simple properties; extract when you add methods, async logic, or many fields.
  • Use Alpine.data('name', () => ({})) for reusable components and document.addEventListener('alpine:init') to register them.
  • Use Alpine.store for global shared state and computed-like getters to keep templates simple.
  • Prefer x-show for toggling visibility and x-if for conditional rendering when DOM removal is required.
  • Use $refs, $dispatch, $watch and $nextTick for interactions instead of embedding complex event logic in attributes.

Example use cases

  • Converting a long x-data object with async fetch into a top-level function that returns component state and methods.
  • Registering a dropdown or modal as Alpine.data to reuse behavior across multiple pages.
  • Creating a cart store with Alpine.store for totals, add/remove methods, and computed getters used by many components.
  • Building a contact form with validation and submit logic extracted into a contactForm() function.
  • Applying transition modifiers for polished enter/leave animations without cluttering templates.

FAQ

When is inline x-data acceptable?

Use inline x-data only for very small state objects (typically 1–3 primitive properties) without methods or async logic.

How do I share state across unrelated components?

Use Alpine.store('name', {...}) registered during alpine:init; stores are accessible via $store in any x-data scope.