Большие фронтенд-фреймворки часто требуют сложной настройки: сборщики, роутинг, состояния, хуки, конфигурации. Но для небольших задач вроде выпадающих меню, модалок или табов этого слишком много — иногда хочется просто взять HTML и быстро добавить в него немного интерактивности.
Здесь отлично подходит Alpine.js — лёгкий (менее 10 KB), быстрый и удобный фреймворк, который добавляет реактивность прямо в HTML. Он похож на Vue.js в миниатюре: декларативные шаблоны и реактивные данные, но без этапа сборки — достаточно подключить <script> и можно сразу использовать.
Если ты знаешь HTML, можешь сразу начинать. В этой статье мы разберём Alpine.js шаг за шагом — от простых примеров до более продвинутых приёмов и полезных плагинов.
Что такое Alpine.js — в двух словах
Alpine позволяет писать поведение прямо в HTML через «директивы» (x-...) и «магические свойства» ($...). Он маленький, понятный и идеально подходит для интерактивных элементов: меню, модалки, таблицы, фильтры и т. п. Официальное «Start here» показывает дух фреймворка: «вставь <script> — и поехали».
Небольшое примечание про стили
Во всех примерах в этой статье используются классы вроде px-3, py-1, bg-pink-200, rounded и другие.
Это классы из фреймворка Tailwind CSS — он не обязателен, но помогает быстро добавлять оформление, чтобы примеры выглядели аккуратно.
Чтобы эти классы работали, нужно подключить Tailwind через Play CDN — самый быстрый способ без настройки сборки:
<script src="https://cdn.tailwindcss.com"></script>
Добавьте эту строчку в <head> вашей страницы перед подключением Alpine.js, и все примеры из статьи будут отображаться с готовым оформлением.
Установка
Через CDN (самый быстрый старт)
<!doctype html>
<html lang="ru">
<head>
<!-- Подключаем актуальный Alpine 3.15.0 -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/dist/cdn.min.js"></script>
<style>
/* x-cloak прячет элементы до инициализации Alpine (без «мигания») */
[x-cloak] { display: none !important; }
</style>
</head>
<body>
<h1 x-data="{ msg: 'Привет, Alpine!' }" x-text="msg" x-cloak></h1>
</body>
</html>
Через npm
Самый простой компонент: x-data и x-text
Чтобы сделать страницу «живой», нужно уметь хранить данные и показывать их на странице. Для этого в Alpine есть x-data (задаёт данные) и x-text (выводит их в HTML). Это основа любого компонента — без них остальное не заработает.
<div x-data="{ likes: 0 }">
<button
@click="likes++"
class="px-3 py-1 bg-pink-200 rounded">
❤️ Лайк
</button>
<p class="mt-2">
Лайков: <span x-text="likes"></span>
</p>
</div>
- Сначала likes равен 0.
- Каждый раз, когда нажимаешь кнопку, @click увеличивает likes на 1.
- x-text сразу показывает новое количество лайков — потому что Alpine сам отслеживает likes и обновляет текст при каждом изменении.
Привязка атрибутов и классов: x-bind
x-bind нужен, чтобы менять внешний вид или свойства элементов, когда меняются данные. Например, пусть у нас есть кнопка, которая включает и выключает «свет». Его часто записывают коротко — например, :class вместо x-bind:class.
Пример: изменение цвета кнопки
<div x-data="{ light: false }">
<button
@click="light = !light"
:class="light ? 'bg-yellow-300' : 'bg-gray-300'"
x-text="light ? 'Выключить свет' : 'Включить свет'"
></button>
</div>
- Сначала light равен false, и кнопка серая — как будто свет выключен.
- Когда мы нажимаем кнопку, @click меняет light на true, и :class делает кнопку жёлтой — свет включён.
Пример: блокировка кнопки при загрузке
<div x-data="{ loading: false }">
<button
@click="loading = true"
:disabled="loading"
class="px-3 py-1 bg-blue-200 rounded">
Отправить
</button>
<p class="mt-2" x-text="loading ? 'Загрузка...' : ''"></p>
</div>
- Сначала loading равен false, и кнопка активна.
- Когда нажимаем её, @click меняет loading на true.
- Атрибут :disabled="loading" делает кнопку неактивной, как только loading становится true.
Показ и скрытие элементов: x-show
x-show нужен, чтобы показывать или скрывать элементы в зависимости от данных. Элемент не удаляется, просто меняется его display, и он исчезает с экрана.
<div x-data="{ visible: false }">
<button @click="visible = !visible">
Показать / Скрыть
</button>
<p x-show="visible" class="mt-2">
Привет! Я появляюсь и исчезаю.
</p>
</div>
- Сначала visible равен false, и абзац скрыт.
- Когда нажимаем кнопку, @click меняет visible на true, и x-show делает элемент видимым.
Удаление и создание элементов: x-if
x-if нужен, чтобы добавлять элемент в HTML только когда он нужен, а не просто прятать его. Когда условие ложное, элемент полностью удаляется из DOM, а не просто скрывается.
<div x-data="{ open: false }">
<button @click="open = !open">
Показать / Скрыть
</button>
<template x-if="open">
<p class="mt-2">
Я создаюсь заново каждый раз, когда появляюсь.
</p>
</template>
</div>
- Сначала open равен false, и элемента вообще нет в HTML.
- Когда нажимаем кнопку, @click меняет open на true, и x-if создаёт абзац.
- Когда снова нажимаем, open становится false, и Alpine удаляет этот абзац из DOM.
В Alpine.js <template> — это невидимый контейнер. С x-if он нужен, чтобы создавать элемент, когда условие true, и удалять его, когда false. В отличие от x-show, который просто прячет элемент стилями, x-if действительно добавляет и убирает его из HTML.
Вывод списков: x-for
x-for нужен, чтобы повторять один и тот же кусок HTML для каждого элемента из массива данных. Так можно легко вывести список товаров, сообщений, ссылок — чего угодно.
<div x-data="{ items: ['Яблоко', 'Банан', 'Апельсин'] }">
<ul>
<template x-for="item in items" :key="item">
<li x-text="item"></li>
</template>
</ul>
</div>
Сначала в x-data мы задали массив items.
- x-for="item in items" берёт каждый элемент массива и создаёт для него отдельный <li>.
- :key="item" помогает Alpine отслеживать, какой элемент какой, если список будет меняться.
- x-text="item" выводит название фрукта внутрь <li>.
Связь с полями ввода: x-model
x-model нужен, чтобы связывать значение поля ввода с переменной. Когда пользователь что-то вводит, данные автоматически обновляются, и наоборот — если данные изменятся, обновится и поле.
<div x-data="{ name: '' }">
<input
type="text"
x-model="name"
placeholder="Введите имя">
<p class="mt-2">
Привет, <span x-text="name || 'гость'"></span>!
</p>
</div>
- Сначала name пустой, и на странице выводится «гость».
- Когда мы начинаем печатать в поле, x-model сразу меняет значение name.
- Alpine следит за этим и автоматически обновляет текст — приветствие меняется прямо во время ввода.
Это и есть двусторонняя связь данных: поле обновляет переменную, а переменная — поле.
Автоматические действия при изменении данных: x-effect
x-effect позволяет реагировать на изменение данных без кликов и событий. Он «следит» за выражением внутри и каждый раз, когда его значение меняется, запускает код. Это похоже на «наблюдателя», который автоматически выполняет нужные действия.
Пример: логирование при изменении счётчика
<div x-data="{ count: 0 }">
<button @click="count++">Увеличить</button>
<p class="mt-2">Счётчик: <span x-text="count"></span></p>
<div x-effect="console.log('Новое значение:', count)"></div>
</div>
- Сначала count равен 0.
- Каждый раз, когда ты нажимаешь кнопку и count увеличивается, x-effect срабатывает и выводит новое значение в консоль.
Внутри x-effect пишется обычное JavaScript-выражение — почти так же, как в @click или x-init. Когда count меняется, x-effect перезапускается и выводит новое значение.
Где это полезно
- Автоматически сохранять данные в localStorage при изменении
- Отправлять запросы при изменении фильтра или поиска
- Переключать классы или анимации без кликов
- Обновлять внешние виджеты, если изменилась переменная
Обращение к элементам: x-ref и $refs
x-ref нужен, чтобы пометить элемент и потом легко получить к нему доступ из кода. А $refs — это объект, в котором хранятся все такие элементы по имени.
<div x-data>
<input x-ref="name" type="text" placeholder="Введите имя">
<button @click="$refs.name.focus()">
Поставить фокус
</button>
</div>
- Здесь мы даём полю ввода ссылку x-ref="name".
- Когда нажимаем кнопку, @click вызывает $refs.name.focus(), и Alpine ставит курсор в это поле.
- Не нужно искать элемент через document.querySelector — Alpine делает всё проще и безопаснее.
Код при загрузке компонента: x-init и init()
Иногда нужно что-то сделать сразу, как только компонент появился на странице — например, подгрузить данные, запустить таймер или показать приветствие. Для этого есть два способа: x-init и init().
Пример с x-init
<div x-data="{ message: '' }" x-init="message = 'Готово!'">
<p x-text="message"></p>
</div>
- Когда компонент загружается, x-init запускает код и записывает 'Готово!' в message.
- Alpine сразу обновляет текст — и на странице появляется сообщение.
Пример с init() внутри x-data
<div x-data="{
message: '',
init() {
this.message = 'Компонент инициализирован'
}
}">
<p x-text="message"></p>
</div>
Метод init() вызывается автоматически при инициализации компонента.
Такой вариант удобен, если нужно выполнить несколько строк кода или что-то посложнее (например, сделать fetch).
📢 Подписывайтесь на наш Telegram-канал.
Там вы найдете анонсы обучающих статей и видео, готовый код для ваших проектов и увлекательные курсы. Ничего лишнего — только практика, вдохновение и развитие.
Разница простая:
- x-init — быстрый способ выполнить одну строку при запуске.
- init() — если нужен настоящий мини-скрипт прямо внутри компонента.
Скрытие элементов до загрузки: x-cloak
Когда страница только загружается, Alpine ещё не успел инициализировать компоненты.
Если в них есть условное отображение (x-show, x-if и т.п.), может случиться неприятное «мигание» — элементы появятся на долю секунды, а потом пропадут.
Чтобы этого не было, используют x-cloak.
Как использовать
<style>
[x-cloak] { display: none !important; }
</style>
<div x-data="{ open: false }">
<button @click="open = !open">Показать</button>
<p x-show="open" x-cloak>
Этот текст не «мигнёт» при загрузке
</p>
</div>
- x-cloak добавляется на элемент, который не должен появляться до инициализации Alpine
- CSS-правило [x-cloak] { display: none !important; } скрывает все такие элементы
- Когда Alpine запускается, он удаляет атрибут x-cloak, и элемент снова становится видимым (если условие x-show выполнено)
Вывод HTML-кода из данных: x-html
x-html нужен, чтобы выводить HTML-код прямо из данных. Он работает как x-text, но не экранирует HTML, а вставляет его «как есть».
Пример
<div x-data="{ content: '<b>Жирный текст</b>' }">
<p x-html="content"></p>
</div>
Здесь x-html вставит содержимое content как настоящий HTML — на странице появится жирный текст, а не буквы <b>.
Если бы мы использовали x-text, получилось бы вот так:
<p x-text="content"></p>
<!-- Вывод: <b>Жирный текст</b> (просто текст, не жирный) -->
Обработчики событий: x-on и @
x-on позволяет выполнять код при событиях — например, при клике или вводе текста. Почти всегда используют короткую запись @ вместо x-on: — так короче и удобнее.
Пример: клик по кнопке
<div x-data="{ count: 0 }">
<button
@click="count++"
class="px-3 py-1 bg-blue-200 rounded">
Нажми
</button>
<p class="mt-2">
Нажатий: <span x-text="count"></span>
</p>
</div>
Здесь @click срабатывает при нажатии на кнопку и увеличивает count. Alpine сам обновляет интерфейс, потому что он следит за count — это реактивность.
Полезные модификаторы событий
Модификаторы — это «дополнения» к событиям, которые меняют их поведение. Они пишутся через точку после события.
<!-- сработает только один раз -->
<button @click.once="alert('Привет!')">Нажми один раз</button>
<!-- остановит всплытие события -->
<button @click.stop="alert('Только эта кнопка')">Стоп</button>
<!-- отменит стандартное поведение -->
<a href="#" @click.prevent="alert('Переход отменён')">Ссылка</a>
<!-- клик вне элемента -->
<div x-data="{ open: true }" @click.outside="open = false">
<p x-show="open">Я закроюсь при клике снаружи</p>
</div>
<!-- нажатие клавиши -->
<input @keyup.enter="alert('Нажата Enter')">
Часто используемые модификаторы x-on / @
📍 Управление выполнением
- .once — срабатывает только один раз
- .prevent — отменяет стандартное поведение браузера (например, переход по ссылке)
- .stop — останавливает всплытие события
- .self — срабатывает только если клик именно по самому элементу (не по вложенным)
📍 Работа с областью события
- .outside — срабатывает, если клик был вне элемента
- .window — слушает событие на window
- .document — слушает событие на document
📍 Клавиши (для @keydown / @keyup)
- .enter — клавиша Enter
- .escape — Esc
- .space — Пробел
- .tab — Tab
- .backspace — Backspace
- .delete — Delete
- .arrow-up, .arrow-down, .arrow-left, .arrow-right — стрелки
- .shift, .ctrl, .alt, .meta — модификаторы клавиш
📌 Эти модификаторы можно комбинировать, например:
<input @keyup.enter.prevent="submitForm()">
<div @click.outside.window="open = false"></div>
Плавные анимации появления: x-transition
x-transition нужен, чтобы добавить плавную анимацию при показе и скрытии элемента. Он работает только вместе с x-show, потому что x-show не удаляет элемент, а просто меняет его display.
Пример
<div x-data="{ open: false }">
<button
@click="open = !open"
class="px-3 py-1 bg-blue-200 rounded">
Показать / Скрыть
</button>
<p
x-show="open"
x-transition
class="mt-2 p-2 bg-green-100 rounded">
Я появляюсь и исчезаю плавно
</p>
</div>
- Когда open становится true, Alpine показывает элемент и добавляет плавное появление.
- Когда false — плавно скрывает его.
- Анимация происходит автоматически, без дополнительного кода или CSS.
📌 Можно тонко настраивать анимацию, добавляя модификаторы:
- x-transition:enter — классы при появлении
- x-transition:leave — классы при скрытии
- x-transition.scale, .opacity, .duration.500ms и т.д.
Пример
<p
x-show="open"
x-transition:enter.scale.80
x-transition:leave.opacity.duration.500ms>
Плавная анимация
</p>
Перенос элементов: x-teleport
x-teleport нужен, чтобы вставить элемент в другое место на странице, не двигая его в коде. Это удобно для модалок, всплывающих меню и других элементов, которые логически находятся внутри компонента, но по HTML-структуре должны быть выше (например, в конце body).
Пример: модальное окно
<div x-data="{ open: false }">
<button
@click="open = true"
class="px-3 py-1 bg-blue-200 rounded">
Открыть модалку
</button>
<template x-teleport="body">
<div
x-show="open"
class="fixed inset-0 bg-black/50 flex items-center justify-center">
<div class="bg-white p-4 rounded shadow">
<p>Привет, я модалка!</p>
<button @click="open = false" class="mt-2 px-3 py-1 bg-gray-200 rounded">
Закрыть
</button>
</div>
</div>
</template>
</div>
Хотя код модалки написан внутри компонента, x-teleport="body" переносит её прямо в конец <body>. Это нужно, чтобы она не ломала верстку и всегда отображалась поверх всего.
Отслеживание изменений: $watch
$watch позволяет следить за изменением конкретной переменной и выполнять код, когда она меняется. Его обычно вызывают внутри x-init или init().
Пример: отслеживаем count
<div x-data="{ count: 0 }" x-init="
$watch('count', value => {
console.log('Новое значение count:', value)
})
">
<button @click="count++" class="px-3 py-1 bg-blue-200 rounded">
Увеличить
</button>
<p>Счётчик: <span x-text="count"></span></p>
</div>
Каждый раз, когда count меняется, $watch вызывает функцию и передаёт новое значение. Это удобно, если нужно запускать код при изменении данных (сохранение, валидация и т.д.).
Отправка событий: $dispatch
$dispatch нужен, чтобы отправлять собственные события, которые могут ловить родительские компоненты.
Пример: отправляем событие liked
<div x-data @liked.window="alert('Кто-то нажал лайк!')">
<button
x-data
@click="$dispatch('liked')"
class="px-3 py-1 bg-pink-200 rounded">
❤️ Лайк
</button>
</div>
- Когда нажимаешь на кнопку, она отправляет событие liked.
- Родитель ловит его с помощью @liked.window.
- Так компоненты могут «общаться» друг с другом, даже если они не связаны напрямую.
Выполнить код после обновления DOM: $nextTick
В Alpine все изменения данных происходят реактивно — ты меняешь значение переменной, а фреймворк потом обновляет HTML. Но обновление DOM не происходит сразу в тот же момент, а чуть позже — после того, как Alpine закончит текущий цикл обновлений.
Если в этот же момент попробовать обратиться к элементу (например, поставить фокус, измерить высоту и т.д.), можно получить старое состояние. Вот тут и нужен $nextTick: он откладывает выполнение кода до тех пор, пока Alpine не обновит DOM.
Пример: поставить фокус после появления элемента
<div x-data="{ open: false }">
<button
@click="
open = true;
$nextTick(() => $refs.input.focus())
"
class="px-3 py-1 bg-blue-200 rounded">
Открыть
</button>
<div x-show="open" class="mt-2">
<input x-ref="input" type="text" placeholder="Введите имя"
class="border px-2 py-1 rounded">
</div>
</div>
Здесь при клике мы:
- Меняем open = true, чтобы показать блок с полем.
- Вызываем $nextTick, чтобы дождаться, когда Alpine отрендерит <input>.
- Уже после обновления DOM вызываем $refs.input.focus().
Если вызвать focus() без $nextTick, Alpine ещё не вставит <input> в DOM, и будет ошибка.
Где ещё применяют $nextTick
- дождаться, когда x-show или x-if создадут элемент
- измерить размеры элемента (offsetHeight, scrollWidth)
- прокрутить скролл (scrollIntoView()) после добавления новых элементов
- запускать сторонние скрипты, которые должны примениться к уже обновлённому HTML
Глобальные сторы: Alpine.store()
Иногда одно и то же состояние нужно в разных местах страницы: тема (светлая/тёмная), корзина, счётчик уведомлений, авторизация и т. п. Держать это в одном x-data неудобно: компоненты получаются связаны. Alpine.store(name, value) создаёт глобальный реактивный стор (хранилище), доступный в любом компоненте через $store.name.
Когда и где объявлять
Инициализируйте стор до старта Alpine, в обработчике alpine:init (CDN-вариант) или сразу перед Alpine.start() (ESM-вариант). Так вы гарантируете, что все компоненты увидят стор.
Вариант через CDN
<!-- Alpine -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('theme', {
dark: false,
toggle() { this.dark = !this.dark }
})
Alpine.store('cart', {
items: [],
add(product) { this.items.push(product) },
remove(index) { this.items.splice(index, 1) },
clear() { this.items = [] },
get count() { return this.items.length }, // вычисляемое свойство
get total() { return this.items.reduce((s,p) => s + p.price, 0) }
})
})
</script>
Теперь в любой части страницы можно обратиться к theme и cart через $store.theme и $store.cart.
Вариант через ESM (бандлер)
// main.js
import Alpine from 'alpinejs'
Alpine.store('theme', {
dark: false,
toggle() { this.dark = !this.dark }
})
Alpine.store('cart', {
items: [],
add(product) { this.items.push(product) },
remove(index) { this.items.splice(index, 1) },
clear() { this.items = [] },
get count() { return this.items.length },
get total() { return this.items.reduce((s,p) => s + p.price, 0) }
})
window.Alpine = Alpine
Alpine.start()
Как использовать сторы в разметке
Чтение и изменение из любого компонента
<!-- Шапка сайта: иконка корзины -->
<div x-data class="flex items-center gap-2">
<span>🛒</span>
<span x-text="$store.cart.count"></span>
<span>тов.</span>
</div>
<!-- Карточка товара: добавляет в корзину -->
<div x-data="{ product: { id: 1, title: 'Кружка', price: 350 } }" class="mt-4">
<p class="font-medium" x-text="product.title"></p>
<p class="text-sm text-gray-600" x-text="product.price + ' ₽'"></p>
<button class="mt-2 px-3 py-1 bg-blue-200 rounded"
@click="$store.cart.add(product)">
В корзину
</button>
</div>
<!-- Подвал/сайдбар: итог -->
<div x-data class="mt-6">
<p>Позиций: <b x-text="$store.cart.count"></b></p>
<p>Итого: <b x-text="$store.cart.total + ' ₽'"></b></p>
<button class="mt-2 px-3 py-1 bg-gray-200 rounded"
:disabled="$store.cart.count === 0"
@click="$store.cart.clear()">
Очистить корзину
</button>
</div>
- Доступ к значениям и методам: $store.cart.*
- Вычисляемые свойства (get total() { ... }) ведут себя как обычные данные — реактивно пересчитываются.
Применение глобального состояния к оформлению
<!-- Переключатель темы -->
<div x-data class="mt-6">
<button class="px-3 py-1 bg-gray-200 rounded"
@click="$store.theme.toggle()">
Переключить тему
</button>
</div>
<!-- Применяем класс 'dark' на <html> или <body> -->
<html :class="$store.theme.dark && 'dark'">
<!-- ... -->
</html>
При изменении $store.theme.dark класс переключится везде, где вы на него ссылаетесь.
Пример «целиком»: список товаров + корзина
<div x-data="{ products: [
{ id: 1, title: 'Кружка', price: 350 },
{ id: 2, title: 'Футболка', price: 1200 },
{ id: 3, title: 'Наклейки', price: 90 }
]}">
<!-- Витрина -->
<div class="grid gap-3 md:grid-cols-3">
<template x-for="p in products" :key="p.id">
<div class="p-3 border rounded">
<div class="font-medium" x-text="p.title"></div>
<div class="text-sm text-gray-600" x-text="p.price + ' ₽'"></div>
<button class="mt-2 px-3 py-1 bg-blue-200 rounded"
@click="$store.cart.add(p)">
В корзину
</button>
</div>
</template>
</div>
<!-- Корзина -->
<div class="mt-6 p-3 border rounded">
<div class="flex items-center gap-2">
<span>🛒</span>
<b x-text="$store.cart.count"></b>
<span>тов.</span>
</div>
<ul class="mt-2 list-disc list-inside">
<template x-for="(item, i) in $store.cart.items" :key="item.id + ':' + i">
<li class="flex items-center justify-between">
<span>
<span x-text="item.title"></span>
— <span x-text="item.price + ' ₽'"></span>
</span>
<button class="px-2 py-0.5 bg-gray-200 rounded"
@click="$store.cart.remove(i)">
Удалить
</button>
</li>
</template>
</ul>
<div class="mt-3">
<b>Итого:</b> <span x-text="$store.cart.total + ' ₽'"></span>
</div>
<button class="mt-3 px-3 py-1 bg-gray-200 rounded"
:disabled="$store.cart.count === 0"
@click="$store.cart.clear()">
Очистить корзину
</button>
</div>
</div>
Про сохранение между перезагрузками
- Если хотите, чтобы значения стора переживали перезагрузку страницы (например, тема или корзина), два практичных пути:
- Простой ручной способ: читать/писать localStorage внутри методов стора (init-подобная инициализация и сохранения при изменениях).
- Плагин Persist: он упрощает хранение значений в localStorage. Чаще его используют в x-data, а для стора удобно вызывать сохранение из компонентов или методов стора. (Если понадобится — напишу готовый шаблон под вашу задачу.)
Комментарии
0