Alpine.js for home-cooked apps

I’m a big fan of home-cooked apps—a term I first heard from author Robin Sloan and have since run with. A home-cooked app is one that you create for yourself, friends, or family. It’s typically not something you are releasing to the general public. For me, this takes the form of various web applications. I gave a talk on home-cooked apps, which you can check out if you want to deep dive.

Here are some examples of home-cooked apps I’ve made:

I have a lot of ideas for these kinds of apps, but I have extremely limited time. With working full-time and managing aspects of my disability, I have precious little time to build these projects. I need to plow through them quickly. I need to play to my strengths—prioritize frontend because I’m better at that, minimize backend logic, and simplify data storage and retrieval.

One aspect of an app that can bog things down is interactive UI. But that’s no problem because I have a secret weapon.

Alpine.js

My brother introduced me to Alpine a couple of years ago when he used it at Storyware. I didn’t see the point of Alpine at first. It’s based on Vue’s reactivity engine, so why not use Vue instead? And what’s with all the inlined JavaScript code? 🤮

But then I used it, and oh man, did it feel like having frontend superpowers. If you’ve never worked with a reactive UI library, prepare to have your mind blown. But even if you have worked with Vue, React, or the other usual suspects, Alpine can still be a breath of fresh air.

Let me give you the obligatory counter button example. First, I’ll pull in Alpine via CDN (no build step needed—grab and go).

1<head>
2 <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
3</head>

And now for the component:

1<div x-data="{ count: 0 }">
2 <p x-text="count"></p>
3
4 <button @click="count++">Add +</button>
5 <button @click="count--">Subtract -</button>
6</div>

Add the x-data attribute to an HTML tag to create an Alpine component anywhere. In this example, we’re initializing our data object with a single variable, count, set to 0.

Then, I’ll create a paragraph element to display the count’s value. By setting its x-text attribute to count, I’m saying, “Make the innerText of this paragraph show the current value of the count variable.”

Then, I’ll add two buttons—one for adding and one for subtracting. I can handle the click event with the @click attribute, which will run its value as a JavaScript expression. In this case, the expression is to increment or decrement the count by one.

As I click the buttons, the value of count is updated, and the DOM automatically refreshes itself to show the current value

Here’s a demo of that component in action.

See the Pen Untitled by Blake Watson (@blakewatson) on CodePen.

I imagine many readers will find this example straightforward—it’s reactive UI 101. But the upside is that it took zero setup, no build, and no npm dependencies—we didn’t even need a JavaScript file!

I will review Alpine in some detail here. These points are relevant whether you’re building a home-cooked app or not. I’m looking at Alpine in that context because that’s how I use it most of the time. And because these projects are solo and small, I don’t need the tooling and type support that other libraries offer. That said, Alpine is flexible and can certainly fit different workflows.

Benefits of Alpine

While you can totally npm install Alpine and bundle it like other packages, I’ll mainly refer to the CDN version. This is where Alpine shines the brightest.

Easy setup

Alpine is easy to install, as we saw in the example. Just include it in a script tag. There’s no build step required. I recommend downloading the CDN version and hosting it yourself rather than hotlinking from the CDN. You can even check the file into your Git repo.[2]

Inline code

Alpine’s most defining and controversial feature is its ability to write much, if not all, of your JavaScript code inline in the HTML. It is both awesome and horrible. Check this out.

See the Pen Alpine.js: inline fetch example by Blake Watson (@blakewatson) on CodePen.

In this example, I’m displaying a link to a specific post of mine on Mastodon. When you click the Preview post button, the code sends a request to the Mastodon API, fetches the data for that individual post, and displays the post’s content on the page.

You can see that I can write multiple lines of JavaScript inline inside the @click attribute. This is super convenient when you need to do something quick and dirty or one-off. But you have to be careful because writing your code this way can get unwieldy fast.

Plugins

Though I haven’t used them, Alpine has some plugins that help you achieve common UI tasks. I won’t list them all here, but to give you an example, the Mask plugin lets you easily limit text in an input to follow a particular pattern (like a date). The Sort plugin lets you apply drag-and-drop sorting to any group of elements.

Powerful

Alpine can help you achieve a lot with a little, so it’s great for building UIs quickly. For example, here’s a todo list where I use as little code as possible.

See the Pen Alpine.js: todo app in 20 lines by Blake Watson (@blakewatson) on CodePen.

First, I define an Alpine component on the <main> element by adding the x-data attribute. I define one variable, tasks, that will represent the array of tasks.

From there, the app is split into two parts: the form for adding tasks and the list for displaying them.

The form needs to track whatever is in the text input, so I’ll add an x-data attribute with the variable task and set it to an empty string. This defines another Alpine component. Anything inside the form has access to task.

By using the x-model attribute on the <input> element, Alpine will automatically bind the input value to the variable, task. When the form is submitted, I handle that event with the @click attribute (.prevent is a shortcut to call preventDefault() on the event, which I need to prevent a page reload). Since I can access the input’s current value via task, I’ll push that value to the tasks array. Finally, I’ll set task to an empty string. This clears the value of the input, thanks to x-model’s two-way binding.

My list of tasks will show up when at least one task exists. The list remains hidden until that time, thanks to the x-show attribute. I’ll use x-for to create a loop inside the list. Alpine requires x-for to be added to a <template> tag. Whatever element you include inside the template tag will be rendered for each item in the list.

In this case, I’ll loop over tasks, defining a variable task for each one. It’s important to note that the variable task here has no relation to the variable task that I defined in the form. This list item isn’t within the scope of the form, so I don’t have access to the form’s x-data values.

Back to the loop, I’ll output an <li> tag for each task using x-text to display the task. I’ll include a checkbox so I can mark tasks as done.

That’s it! This is an admittedly barebones todo app (you can’t edit tasks or even delete them), but thanks to Alpine’s power, I was able to complete it in 20 lines.

Drawbacks of Alpine

Alpine shines when supplementing existing HTML. That said, as you saw in my todo app example, it’s possible and quite easy to render HTML on the client side with Alpine. Conventional wisdom says the more HTML you can pre-render, the better. But when you need it, Alpine can do some heavy lifting. This is a feature but also a drawback because the more client-side HTML rendering you use, the slower your page, the lesser your SEO, and, potentially, the lesser your page’s accessibility.

Despite the homepage calling it “lightweight,” I think the library is a bit chunky at 109kb. That said, its gzipped size is about 22kb, as best I can tell. In this age of massive JavaScript bundles, this probably isn’t too bad. Still, it’s something to consider. You might not want to pull in Alpine if you have an isolated case where a few lines of vanilla JavaScript would do the trick.

Finally, despite Alpine’s inline JavaScript capability being a feature, it can also be a drawback. The more JavaScript you stuff into your HTML code, the more unreadable it becomes. It can quickly grow unwieldy. And you’ll most likely be missing out on editor support for JavaScript—the kind you would expect inside a <script> tag or standalone JavaScript file. And not only that. If you’re in an environment that has a content security policy disallowing unsafe-eval, you can’t use the inline syntax at all. Fortunately, Alpine has an answer to both of these scenarios—a CSP-friendly build that doesn’t rely on unsafe-eval and Alpine.data, a way to define component behavior in JavaScript rather than inline HTML (we’ll look at this a bit later).

Alpine alternatives

I’ve heard Alpine being called “the new jQuery,” which makes sense because you can get up and running with minimal fuss, the old-school way before the days of webpack, npm, and friends. jQuery itself isn’t needed as much these days, seeing as vanilla JavaScript is more powerful than it used to be. But vanilla JavaScript is worth mentioning as an alternative.[3] You might be able to accomplish your goals more easily than you thought. It’s always a good idea to consider whether you even need to pull a third-party library into your project.

Vue

When it comes to reactive UI libraries, the closest alternative to Alpine is Vue. Although, it’s probably more accurate to say Alpine is an alternative to Vue since Alpine is built atop Vue’s reactivity engine.

Vue has long been my preferred JavaScript framework. In my opinion, Vue’s “progressive” nature (i.e., its ability to be used incrementally in more complex ways) is grossly underrated. For example, a totally valid way to use Vue is to grab the CDN version and start using it on your page—just like I did with Alpine in the demos I’ve shown.

I think Alpine has the upper hand in cases like home-cooked apps where you want to blaze through some UI quickly, whereas Vue is better if your project is larger and needs more reusable frontend components.[4]

React and friends

I’m not a React guy, but it’s the most popular way to build reactive frontend UI. I will lump other reactive libraries in this group—Preact, Angular, Solid, Svelte, etc. These frameworks shine when they have their tooling available. I’m trying to avoid the complexity of build steps for most of my projects these days. I’m sure these libraries are great, but they’re not for me. You do you, though.

Reef

Reef is a little reactive library from Chris Ferdinandi. I like it because—surprise—there’s no build step. It follows the familiar reactivity patterns of other libraries by giving you the tools needed to observe state (i.e., variables) and re-render the UI when the state changes. It’s nice, simple, and a great use of JavaScript’s template literals.

Lit

If components are what you’re after, Lit is a great choice. Lit is a library from Google that extends native web components with convenient features like better reactive props and the ability to pass data to components via props. It also uses JavaScript template literals and efficiently updates the DOM when your component state changes. Lit docs favor TypeScript, but they have JavaScript examples as well. You can use Lit without a build step.

Alpine for the organized

As cool as it is to do so much stuff inline, looking at tons of JavaScript in my HTML makes my eyes twitch. I come from the old school where we were taught the separation of concerns. It all depends on what I’m doing, but if I have a significant amount of business logic, I like to externalize some of the JavaScript to get it out of the template.

To give you an example, I will add some features to the todo list example from earlier. I’ll add the ability to edit and delete tasks and show a status message that displays a few stats about the tasks. I’ll externalize much of the JavaScript in this example using Alpine.data.

See the Pen Alpine.js: todo app improved by Blake Watson (@blakewatson) on CodePen.

In this example, I listen for the alpine:init event and use Alpine.data to define a component in the JavaScript. There, I store the array of tasks, the new task input field, the status message, and various methods for interacting with the list of tasks.

I’ve expanded the tasks to be objects rather than strings so that I can track which ones are completed. On the <main> component, I used the x-effect attribute. x-effect takes a JavaScript expression or a method name. In this case, I gave it a method name, effects. Alpine will run the effects method every time one of its dependencies changes (i.e., if one of the variables it uses changes). I’m using it to generate a status message that will update itself automagically as the task counts update.

The new task input works similarly to the old one, except now a task is an object. I’m tracking whether a task is completed and whether it’s currently in edit mode. Adding a task creates a new task object and pushes it to the array of tasks. I’m also giving each task a unique ID.

Once a task is added, the ul’s x-show attribute will become truthy and the list will appear. Like the last example, I’m using x-for to loop over the tasks. I’m passing the task ID as the key. This will help Alpine track each task so that it knows which DOM elements to update when I start editing or removing tasks.

I have two elements inside the list item. One displays the task, and the other edits a task (this one is initially hidden).

When displaying a task, I’m hooking up the checkbox with x-model="task.completed". When I check a task, its completed value updates, which causes my effects method to run again and update the status message.

Then, I provide two buttons, “edit” and “delete.” Clicking the delete button runs the deleteTask method, passing the current task to it so it can filter it out from the list.

Clicking the edit button puts the task in editing mode. That causes the task editing form to appear. The edit form has an input value bound directly to the task’s text property via the x-model attribute. As I type in the input, the task text is directly updated. The task is taken out of edit mode when I submit the form.

And that’s that! That last example might have been a little overwhelming if you’re new to reactive UI. However, it demonstrates that Alpine can handle more complex UI elegantly if you practice discipline.

Use whatever works for you

I’ve enjoyed using Alpine on my home-cooked apps and felt like talking about it. But as always, you should use whatever tools help you get the job done. Shipping is the name of the game for home-cooked apps.

Remember, if you’re working on public-facing websites or apps, ensure a good, accessible user experience. Alpine is great for those projects, too! I don’t want you to think it’s only suitable for personal or toy projects. It all depends on what you need. But for rapid creation without a build step, Alpine really shines.

If you’ve made anything cool with Alpine, tell me about it in email or on Mastodon.


  1. The actual application form doesn’t qualify as a home-cooked app as it’s designed for public use. But the site’s admin area, where Matt and I sort through applications, make notes, and update the status of applications, is very much a home-cooked thing. ↩︎

  2. Before the days when everything was an NPM package, I managed dependencies by downloading pre-built bundles and checking them into Git. That was peak dependency management if you ask me. ↩︎

  3. When I say “vanilla,” I mean JavaScript without a framework. Not a framework called “Vanilla JavaScript.” The site I linked to is a tongue-in-cheek way to say, “JavaScript is pretty nice on its own now.” ↩︎

  4. Alpine doesn’t have as robust support for reusable frontend components as other frameworks. Alpine’s primary use case is supplementing backend-generated templates with interactivity. That said, Alpine is capable of reusing component logic with Alpine.data and if you really want to reuse markup too, you can do that by extending Alpine as demonstrated in this CodePen where I create an x-component directive. ↩︎