Alpine.js is a minimal, declarative JavaScript framework by Caleb Porzio (2019). At just 7.1 kB gzipped — no build step, no virtual DOM — it adds reactive behaviour directly to HTML markup. It’s built for server-rendered apps and static sites, not as a React or Vue replacement.
- 1 Alpine.js is as small as just 7.1 kB gzipped, approximately 18x smaller than React (~130 kB minified) and 13x smaller than jQuery (~90 kB minified).
- 2 It has over 30k+ GitHub stars and is downloaded about 433K+ times a week by npm.
- 3 Alpine.js comes with 15 directives, 6 magic properties, and 2 methods with the full public API.
- 4 It has no build step - just a single <script> tag and you are off.
- 5 Alpine.js is not an alternative to React or Vue in larger single-page applications, and is intended to be used to progressively enhance pages rendered on the server.
What Is Alpine.js?
Alpine.JS is a lightweight, open source JavaScript framework that offers declarative/reactive interactivity in the form of HTML. It was written by Caleb Porzio and initially published in 2019, and is frequently called the jQuery of the modern web, in that it is simple to include without a build system, but based on modern principles of reactivity instead of imperative manipulation of the DOM.
In contrast to React and Vue, which work with a virtual DOM and componentize it, Alpine.js works with the real DOM itself. It scans the page to find x- prefixed attributes and replaces them with reactive behavior, but does not replace HTML server transmissions to browser.
This makes Alpine.js uniquely well-suited for:
- Server-rendered frameworks such as Laravel (Blade), Ruby on Rails (ERB), Django, and AdonisJS
- Static sites built with 11ty, Hugo, or Jekyll that need a sprinkle of JavaScript
- WordPress and Shopify themes requiring lightweight interactivity without a bundler
- Prototyping — where speed of implementation matters more than architectural purity
Alpine.js vs React vs Vue: At a Glance
| Factor | Alpine.js | React | Vue 3 |
|---|---|---|---|
| Bundle size (gzipped) | ~7.1 kB | ~130 kB | ~34 kB |
| Build step required | No | Yes | Optional |
| Virtual DOM | No | Yes | Yes |
| Weekly npm downloads | ~433K+ | ~20 million+ | ~7 million+ |
| GitHub stars | 30,000+ | 220,000+ | 47,000+ |
| Best for | Server-rendered pages, static sites | Large SPAs, complex UIs | Medium apps, progressive adoption |
| Learning curve | Very low | Medium-high | Low-medium |
| State management | Local component state | useState / Redux / Zustand | Pinia / Vuex |
| Full SPA support | Not intended | Yes | Yes |
Sources: npm trends (March 2025), GitHub repository data (March 2025), bundlephobia.com
How to Install Alpine.js
Alpine.js can be added to any project in two ways.
Option 1: CDN (Recommended for most use cases)
Add a single script tag to your HTML <head>. The defer attribute is required so Alpine initializes after the DOM is parsed:
1 | <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> |
No npm, no Webpack, no Vite — that is the complete installation.
Option 2: npm (For project bundlers)
1 | npm install alpinejs |
Then initialize it in your JavaScript entry point:
1 2 3 | import Alpine from 'alpinejs' window.Alpine = Alpine Alpine.start() |
The window.Alpine = Alpine assignment is important if you want to access the Alpine instance from browser DevTools or external scripts.
Core Directives: The Full API in Practice
Alpine.js ships with exactly 15 directives (HTML attributes prefixed with x-). Here are the most commonly used ones, with working examples:
x-data — Declare a Component
x-data defines a new Alpine component and its reactive state. Any element with x-data becomes an isolated reactive scope:
1 2 3 | <div x-data="{ open: false, count: 0 }"> <!-- All children can access 'open' and 'count' --> </div> |
x-bind — Bind HTML Attributes
x-bind (shorthand: : ) dynamically sets any HTML attribute based on the component’s state:
1 2 3 | <button :class="open ? 'bg-blue-500' : 'bg-gray-400'"> Toggle </button> |
x-on — Attach Event Listeners
x-on (shorthand: @) attaches event listeners to elements:
1 | <button @click="open = !open">Toggle Menu</button> |
x-show — Conditional Visibility
x-show toggles the CSS display property based on an expression. Unlike x-if, it does not remove the element from the DOM — it simply hides it:
1 2 3 | <div x-show="open" x-transition> This content is visible when open is true. </div> |
x-model — Two-Way Data Binding
x-model syncs an input element’s value with a reactive data property:
1 2 | <input type="text" x-model="searchQuery"> <p x-text="'You typed: ' + searchQuery"></p> |
x-for — Loop Over Arrays
x-for iterates over an array and renders a template for each item:
1 2 3 | <template x-for="item in items" :key="item.id"> <li x-text="item.name"></li> </template> |
A Real-World Example: Interactive Todo List
The following example combines x-data, x-model, x-for, x-on, and x-transition to build a functional, animated todo list — in pure HTML, with no build step:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <div x-data="{ todos: ['Learn Alpine.js', 'Ship a project'], newTodo: '', addTodo() { if (this.newTodo.trim()) { this.todos.push(this.newTodo.trim()); this.newTodo = ''; } } }"> <form @submit.prevent="addTodo()"> <input type="text" x-model="newTodo" placeholder="Add a todo..." class="border rounded px-3 py-2" > <button type="submit" class="ml-2 px-4 py-2 bg-blue-500 text-white rounded"> Add </button> </form> <ul class="mt-4 space-y-2"> <template x-for="todo in todos" :key="todo"> <li x-text="todo" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 -translate-y-1" x-transition:enter-end="opacity-100 translate-y-0" class="p-2 bg-gray-50 rounded" ></li> </template> </ul> </div> |
This pattern — reactive state declared inline with x-data, form handling with @submit.prevent, and animated list rendering with x-for + x-transition — covers 80% of real-world Alpine.js use cases.
Alpine.js Plugins (Extended Ecosystem)
Alpine.js v3 introduced an official plugin architecture. The following first-party plugins extend the core without bloating the default bundle:
| Plugin | What it adds |
|---|---|
| @alpinejs/persist | Persists x-data state to localStorage across page loads |
| @alpinejs/intersect | Triggers callbacks when elements enter or leave the viewport |
| @alpinejs/resize | Reacts to element size changes via ResizeObserver |
| @alpinejs/focus | Manages focus trapping for modals and dropdowns |
| @alpinejs/collapse | Smooth height animation for accordion/collapsible elements |
| @alpinejs/morph | Intelligently updates the DOM while preserving existing state |
Each plugin is tree-shakable and loaded independently, so only the functionality your project uses is included in the final bundle.
When to Use Alpine.js (and When Not To)
Use Alpine.js when:
- You are dealing with a server-rendered application (Laravel, Rails, Django) and have to implement some UI interaction that will not require overriding the HTML given by the server.
- Your page requires isolated interactive components — a dropdown, a modal, a tab panel, a form with validation — rather than a full client-side router.
- You do not want to go through a build phase and deliver straight to production out of raw HTML.
- You are maintaining a WordPress, Shopify, or static site and React or Vue feels like too much infrastructure.
Do not use Alpine.js when:
- Your application is a full single-page application with client-side routing — React, Vue or SvelteKit should be used instead.
- You need complex, shared global state across many components — Alpine’s state is deliberately local, not designed for large state graphs.
- Your team already has a React or Vue codebase — there is no advantage to mixing frameworks.
As the official Alpine.js documentation puts it: “Think of it like jQuery for the modern web. Plop in a script tag and get going.”
Performance: What 7.1 kB Actually Means
Alpine.js’s minimal footprint has a measurable impact on Core Web Vitals, particularly Time to Interactive (TTI) and Total Blocking Time (TBT) — two metrics Google uses as ranking signals since the 2021 Page Experience update.
A page loading React (130 kB), even with code splitting, requires the browser to parse, compile, and execute significantly more JavaScript than a page using Alpine.js (7.1 kB). On a mid-range mobile device on a 4G connection, this difference can represent 300–600ms of additional processing time — enough to shift a page from a “Good” to a “Needs Improvement” Core Web Vitals rating.
For content sites, blogs, marketing pages, and documentation — where most pages are statically rendered and only need lightweight interactivity — Alpine.js is frequently the correct technical choice over a full SPA framework.
Managing State in Alpine.js
Alpine.js keeps state close to the HTML that uses it, which is one of the reasons it feels simple compared with larger JavaScript frameworks. For a small component, state can be declared directly inside x-data and accessed by the elements below it.
This works well for things such as dropdowns, tabs, modals, filters, and simple forms. Developers can see the state and the markup together instead of moving between several files just to understand how a small interaction works.
As the component becomes more involved, it helps to keep the state organized. Methods can be defined inside x-data when they are only relevant to that component. This keeps related behavior together and avoids creating JavaScript code that has no clear connection to the UI.
For example, a product filter might keep the selected category, search value, and visibility state in the same Alpine component. When one value changes, the related elements can react automatically.
The main advantage is not that Alpine.js provides a completely different way to manage state. It simply keeps state management lightweight enough for interfaces that do not need a larger application architecture.
Working With Alpine.js and Server-Side Applications
I think working with Alpine.js is great in the context where server already renders most of the page. You don’t have to adjust your frontend code around some JS framework – just add Alpine.js directives on top of what you have
Laravel application, for example, would still utilize most of its blades templates to render HTML content. But then Alpine.js would allow you to do stuff like open dropdowns, modals, tabs, work with forms – implement small niceties that make a page more interactive.
You don’t have to adjust your backend code at all to accommodate some frontend JS framework, which makes it much easier to adopt, especially for projects where the server-sidetemplating is the main asset.
You don’t have to learn a new way of building applications – if you are comfortable working with Laravel blades, you can just keep doing that and use Alpine.js as an extension. Same goes for a CMS-driven website which is mostly static HTML but requires some JS programming now and then – use Alpine.js to sprinkle some JS on the page rather than rebuilding the whole thing as a single-page app.
Testing Alpine.js Interactions
Small interactions between a little piece of JavaScript and a webpage can cause big problems if you don’t test them. A dropdown working fine on one page could completely break when another component also tries to access the same state or there are multiple copies of the same component on the same page.
Testing should focus on the interactions that users care about. A modal should open/close, a form should update something else’s state, and an element should only show up if a specific value is correct.
You should also test out the behavior of Alpine.js in conjunction with server rendered HTML. Since Alpine.js binds to elements already present in the DOM, an updated template could result in broken selectors or invalid Alpine.js directives.
Testing becomes a lot easier if you don’t add much logic yourself. It’s easier to reason with and verify that a components state or a directives behavior is correct compared to an entire block of inline Alpine.js code.
Conclusion
Alpine.js occupies an actual and unmet niche in the JavaScript landscape: a lightweight no-build-step library to make server-rendered HTML reactive. With a size of 7.1 kB gzipped, and 15 directives and a star community size of 30,000+, it has proven itself the default TALL stack, and any project where React or Vue would have made architectural overkill.
Alpine.js is probably the right tool to use in the case of your project is a server-rendered application that requires dropdowns or modals or tabs or form interactions, and you do not require client-side routing or a complicated global state. React, Vue, or SvelteKit should be used in case you are developing a large and client-intensive single-page application.
Sources
- Alpine.js official documentation: alpinejs.dev
- Alpine.js GitHub repository: github.com/alpinejs/alpine
- Bundle size data: bundlephobia.com
- Medium: “Alpine.js: The Minimalist JavaScript Framework for Modern Web Development” (Zulfikar Ditya, March 2025)
