Skills alpine
install
source · Clone the upstream repo
git clone https://github.com/TerminalSkills/skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/TerminalSkills/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/alpine" ~/.claude/skills/terminalskills-skills-alpine && rm -rf "$T"
manifest:
skills/alpine/SKILL.mdsource content
Alpine.js
Alpine.js adds reactive behavior directly in HTML markup using
x- attributes. It's ideal for adding interactivity to server-rendered pages without a build step or SPA framework.
Installation
<!-- index.html — add Alpine via CDN --> <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> <!-- Or: npm install alpinejs -->
// main.js — npm module setup import Alpine from 'alpinejs'; window.Alpine = Alpine; Alpine.start();
Core Directives
<!-- templates/basics.html — fundamental Alpine directives --> <div x-data="{ open: false, count: 0 }"> <!-- Toggle visibility --> <button @click="open = !open">Toggle</button> <div x-show="open" x-transition> <p>This content can be toggled</p> </div> <!-- Reactive counter --> <p>Count: <span x-text="count"></span></p> <button @click="count++">Increment</button> <!-- Conditional rendering (removes from DOM) --> <template x-if="count > 5"> <p>Count is greater than 5!</p> </template> </div>
Data Binding
<!-- templates/binding.html — two-way binding and attribute binding --> <div x-data="{ name: '', color: 'blue' }"> <!-- Two-way binding --> <input x-model="name" placeholder="Your name" /> <p>Hello, <span x-text="name || 'stranger'"></span>!</p> <!-- Attribute binding --> <div :class="{ 'text-red': color === 'red', 'text-blue': color === 'blue' }"> Colored text </div> <select x-model="color"> <option value="blue">Blue</option> <option value="red">Red</option> </select> <!-- Style binding --> <div :style="{ color: color, fontWeight: name ? 'bold' : 'normal' }"> Dynamic styles </div> </div>
Loops
<!-- templates/loops.html — iterating over data --> <div x-data="{ items: ['Apples', 'Bananas', 'Cherries'], newItem: '' }"> <ul> <template x-for="(item, index) in items" :key="index"> <li> <span x-text="item"></span> <button @click="items.splice(index, 1)">×</button> </li> </template> </ul> <form @submit.prevent="items.push(newItem); newItem = ''"> <input x-model="newItem" placeholder="Add item" /> <button type="submit">Add</button> </form> </div>
Event Handling
<!-- templates/events.html — event modifiers and custom events --> <div x-data="{ count: 0 }"> <!-- Modifiers --> <button @click.prevent="count++">Prevent default</button> <button @click.once="alert('Only once!')">Click once</button> <input @keydown.enter="submitForm()" @keydown.escape="cancel()" /> <!-- Debounce --> <input @input.debounce.300ms="search($event.target.value)" placeholder="Search..." /> <!-- Listen to events from window --> <div @custom-event.window="count++"> Count: <span x-text="count"></span> </div> <!-- Dispatch custom event --> <button @click="$dispatch('custom-event')">Dispatch</button> </div>
Component Patterns
<!-- templates/dropdown.html — dropdown component --> <div x-data="{ open: false }" @click.outside="open = false"> <button @click="open = !open"> Menu <span :class="{ 'rotate-180': open }" x-text="open ? '▲' : '▼'"></span> </button> <div x-show="open" x-transition.origin.top.left @keydown.escape.window="open = false" class="dropdown-menu"> <a href="/profile">Profile</a> <a href="/settings">Settings</a> <button @click="$dispatch('logout')">Logout</button> </div> </div>
<!-- templates/tabs.html — tabs component --> <div x-data="{ activeTab: 'general' }"> <nav> <button @click="activeTab = 'general'" :class="{ active: activeTab === 'general' }">General</button> <button @click="activeTab = 'security'" :class="{ active: activeTab === 'security' }">Security</button> </nav> <div x-show="activeTab === 'general'">General settings...</div> <div x-show="activeTab === 'security'">Security settings...</div> </div>
Reusable Data with Alpine.data
<!-- templates/reusable.html — extracting reusable components --> <script> // Register reusable component document.addEventListener('alpine:init', () => { Alpine.data('todoList', () => ({ items: [], newItem: '', add() { if (this.newItem.trim()) { this.items.push({ text: this.newItem, done: false }); this.newItem = ''; } }, remove(index) { this.items.splice(index, 1); }, get remaining() { return this.items.filter(i => !i.done).length; }, })); }); </script> <!-- Use anywhere --> <div x-data="todoList"> <form @submit.prevent="add"> <input x-model="newItem" placeholder="New todo" /> <button type="submit">Add</button> </form> <p x-text="`${remaining} remaining`"></p> <template x-for="(item, i) in items" :key="i"> <div> <input type="checkbox" x-model="item.done" /> <span x-text="item.text" :class="{ 'line-through': item.done }"></span> <button @click="remove(i)">×</button> </div> </template> </div>
Working with htmx
<!-- templates/alpine-htmx.html — Alpine + htmx together --> <div x-data="{ editing: false }" id="article-42"> <div x-show="!editing"> <h2>Article Title</h2> <button @click="editing = true">Edit</button> <button hx-delete="/articles/42" hx-target="#article-42" hx-swap="outerHTML">Delete</button> </div> <form x-show="editing" hx-put="/articles/42" hx-target="#article-42" hx-swap="outerHTML"> <input name="title" value="Article Title" /> <button type="submit">Save</button> <button type="button" @click="editing = false">Cancel</button> </form> </div>
Stores (Global State)
<!-- templates/stores.html — Alpine stores for shared state --> <script> document.addEventListener('alpine:init', () => { Alpine.store('notifications', { items: [], add(msg) { this.items.push({ text: msg, id: Date.now() }) }, remove(id) { this.items = this.items.filter(n => n.id !== id) }, }); }); </script> <div x-data @click="$store.notifications.add('Button clicked!')">Click me</div> <div x-data> <template x-for="n in $store.notifications.items" :key="n.id"> <div class="toast" x-text="n.text" @click="$store.notifications.remove(n.id)"></div> </template> </div>
Magic Properties
<!-- templates/magic.html — Alpine magic properties --> <div x-data="{ items: [] }"> <!-- $refs — reference DOM elements --> <input x-ref="input" /> <button @click="$refs.input.focus()">Focus input</button> <!-- $nextTick — run after DOM update --> <button @click="items.push('new'); $nextTick(() => $refs.list.scrollTo(0, 99999))"> Add & scroll </button> <div x-ref="list" style="max-height:200px;overflow:auto"> <template x-for="item in items"><p x-text="item"></p></template> </div> <!-- $watch — react to data changes --> <div x-init="$watch('items', (val) => console.log('items changed:', val))"></div> </div>
Key Patterns
- Use
on a parent element to define reactive scope — everything inside shares that statex-data - Use
for toggling visibility (CSS),x-show
for conditional DOM insertionx-if - Use
for two-way binding on inputs, selects, checkboxesx-model - Use event modifiers (
,.prevent
,.stop
,.debounce
) to reduce boilerplate.outside - Use
to extract reusable components with methods and computed propertiesAlpine.data() - Use
for global state shared across componentsAlpine.store() - Pairs excellently with htmx: Alpine handles UI state, htmx handles server communication