. The is:inline directive prevents Astro from bundling the third-party runtime, which preserves the defer behavior. Once loaded, Alpine attaches to any x-data block in Astro components, .md content, and .mdx content. For heavier interactivity on the same page, mix Astro's framework islands (React, Vue, Svelte) with Alpine for the lightweight surfaces."}},{"@type":"Question","name":"Will Alpine slow down my blog's Lighthouse score?","acceptedAnswer":{"@type":"Answer","text":"No, when installed correctly. The official install includes the defer attribute, so the 15kb runtime downloads in parallel with HTML parsing and executes after the DOM is ready. Alpine does not block First Contentful Paint or Largest Contentful Paint. Add the x-cloak pattern (one CSS rule plus x-cloak on top-level blocks) to eliminate the brief flash of unprocessed content during initial directive binding. On a typical Hugo or Astro blog the Lighthouse Performance score difference before and after adding Alpine is within the measurement noise floor."}}]}
Use case

Alpine.js for Content Creators: No-Build JS Interactivity for Your Hugo, Astro, Eleventy, or Jekyll Blog (2026)

Alpine.js for content creators in 2026 — drop-in JS interactivity for Hugo, Astro, Eleventy, Jekyll, and MDX blogs without a React build pipeline. 10 patterns inside.

26 min read·Updated 2026

Alpine.js lets a content creator sprinkle JavaScript interactivity onto a Hugo, Jekyll, Eleventy, or Astro blog without committing to a React, Vue, or Svelte build pipeline. The trade is twenty minutes of reading against every interactive form, drawer, toggle, and accordion you ever ship on a static site again. This tutorial covers Alpine.js specifically through the lens of a writer, YouTuber, course creator, indie publisher, or newsletter operator whose stack is markdown-driven and whose interactivity needs are small but real: one signup form, one dark-mode toggle, one FAQ accordion, one course-content drawer. Three creator-specific wins drive the case. There is no build step (drop a <script defer> tag in your head partial and you are done). The runtime is tiny (Alpine 3.x is roughly 15kb minified and gzipped, smaller than a single hero image on most blogs). And you learn it in an hour because the entire API is fifteen directives that read like HTML attributes. Here is how to use Alpine in 2026 without picking the wrong CDN, without bloating your Lighthouse score, and without committing your markdown-driven blog to a JavaScript framework you do not need.

Alpine.js for content creators: drop-in patterns you'll actually use

The table below summarizes the day-one interactivity needs every content creator hits on a static-site stack, and what changes when Alpine.js becomes the interactivity layer instead of jQuery, vanilla addEventListener callbacks, or a full SPA framework. The behavior reflects the documented design of Alpine 3.x (current in 2026 per alpinejs.dev) and its fifteen-directive API surface.

Creator interactivity needWithout a frameworkAlpine.js behaviorWhy it matters on a creator blog
Newsletter signup formHand-rolled addEventListener('submit') + fetch + statex-data="{ email: '', sent: false }" + x-on:submit.prevent + fetch to ConvertKit/Mailchimp/ButtondownOne form template works across every post, every landing page, every footer. State lives inline in the markup.
Dark-mode toggleprefers-color-scheme listener + localStorage + class swap + FOUC handlingx-data="{ dark: localStorage.theme === 'dark' }" + x-init + x-on:click + x-bind:classHalf your readers default to dark on iOS. Alpine sets the class on <html> before paint via x-init, no flash.
FAQ accordionjQuery slideToggle or hand-rolled height transitionsx-data="{ open: false }" + x-on:click="open = !open" + x-show + x-transitionSix FAQs per post, FAQPage schema-friendly, no jQuery.
Lazy YouTube embedCustom IntersectionObserver + iframe injectionx-data="{ shown: false }" + x-intersect (plugin) or x-on:click + thumbnail-first patternOne YouTube embed costs ~500kb of JS. Lazy-loading them is the single biggest Lighthouse win on a tutorial blog.
Image carouselSlick / Glide / Swiper (50-200kb each)x-data="{ idx: 0, slides: [...] }" + x-on:click + x-bind:classCarousel logic in 20 lines of HTML attributes, no extra library, no extra bundle.
Copy-link buttonHand-rolled navigator.clipboard callbackx-data="{ copied: false }" + x-on:click="navigator.clipboard.writeText(location.href); copied = true; setTimeout(() => copied = false, 2000)"The "copy link to this section" pattern every long-form blog needs.
Course-content drawerSlide-in panel via CSS + JS coordinationx-data="{ open: false }" + x-show + x-transition.duration.300msA course site's table-of-contents drawer that does not require shipping a SPA.
Cost / license$ for a framework consultant or learning ReactMIT-licensed, free for commercial use, per github.com/alpinejs/alpineA creator running on coffee money keeps the budget for hosting and the email tool.

Casey here. I am the creator version of the user this page is written for. I run a Hugo blog at my domain, a course-delivery microsite that lives on Astro, an Eleventy-built newsletter archive, and a one-page Jekyll experiment I have not killed yet because it still ranks. Every one of those sites needs the same five things to be interactive: a signup form, a dark-mode toggle, FAQ accordions, lazy YouTube embeds, and a copy-link button. For two years I shipped a different solution on each site. jQuery on Jekyll, Preact on Astro, vanilla JS on Hugo, a half-built Vue island on Eleventy. The week I moved every one of them to Alpine.js, I deleted four package.json dev-dep blocks and the sites got faster. The rest of this tutorial is what I wish someone had told me before I learned Alpine from a tutorial written for a developer building a B2B SaaS dashboard.

Why creators pick Alpine over React, Vue, Svelte, or Chakra UI

Most Alpine tutorials are written for the engineer who has just heard about a lightweight framework and wants to compare it against Vue. You probably are not that engineer. You are running a content business. Your stack is a static-site generator that produces HTML. Your interactivity needs are small. The constraint is not Alpine's API surface. The constraint is that you cannot afford to commit your markdown-driven blog to a JavaScript framework's build pipeline for the sake of one signup form. Alpine wins on four creator-specific stakes that the generic comparison tutorials never name.

First, no build step. The official install (per alpinejs.dev) is one line of HTML you paste into your <head> partial: <script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>. There is no npm install, no package.json update, no webpack, no vite, no rollup, no esbuild. Your Hugo site goes from zero interactivity to working interactivity in the time it takes to git-commit the partial. React, Vue, and Svelte all require a build step and a hydration model. Even Astro's island architecture, which is closer in spirit to Alpine, still requires you to learn a framework's component syntax and the Astro-specific client: directives. Alpine reads as HTML attributes. If you can write HTML, you can write Alpine.

Second, the footprint is tiny. Alpine 3.x ships at roughly 15kb minified and gzipped. A single hero image on most content-creator blogs is bigger than that. React plus React-DOM together are roughly 45kb gzipped before you write a single line of your own component code, and that is before you add a router or a state library. On a static-site creator blog, Lighthouse and Core Web Vitals are not a vanity metric. Google uses them as a ranking input. Shipping 15kb of JS that loads with defer and does not block first paint is a measurable SEO win compared to shipping 45kb-plus of framework runtime plus your own bundle.

Third, you learn it in an hour. The entire Alpine API is fifteen directives. The official alpinejs.dev homepage lists them in one bullet column: x-data, x-bind, x-on, x-text, x-html, x-model, x-show, x-transition, x-for, x-if, x-init, x-effect, x-ref, x-cloak, x-ignore. Once you understand x-data (declares reactive state) and x-on (binds events), you can write 80 percent of the Alpine you will ever need. The Scrimba course is one hour. The freeCodeCamp course is one hour. The official "Start Here" walkthrough on alpinejs.dev is shorter than a coffee break. Compare to React: hooks, JSX, virtual DOM, reconciler, refs, effects, suspense, server components. Picking Alpine is picking the framework whose entire surface fits in your head.

Fourth, Alpine plays with markdown-driven content. Your blog posts live in .md files. Most static-site generators (Hugo, Eleventy, Jekyll, Astro, MDX) will pass raw HTML through to the rendered output. That means you can drop an <div x-data="{ open: false }"> block directly into a markdown post and Alpine picks it up. No JSX, no MDX-specific component imports, no framework-aware build step needed inside the markdown file. The interactivity travels with the content. When a writer-collaborator opens the .md file in a future session, they see HTML, not a framework abstraction.

Casey: the week I dropped Preact from my Astro course site and replaced the islands with Alpine, my First Contentful Paint dropped from 1.8 seconds to 1.1 seconds on a throttled 4G test. The site shipped 33kb less JavaScript on the homepage. The signup form still works. The dark-mode toggle still works. The FAQ accordion still works. I never went back.

Alpine.js disambiguation primer plus the install you should actually use

Quick disambiguation before the install, because this trips up creators who google "alpine for my site" and end up reading about a Linux distribution.

Alpine.js is the JavaScript framework at alpinejs.dev, MIT-licensed, current at version 3.x in 2026, maintained by Caleb Porzio and contributors. It is the framework this page is about.

Alpine Linux is a separate project (alpinelinux.org), a security-oriented small-footprint Linux distribution used in Docker images. Not relevant to a content creator's blog.

Alpine the ski resort, Alpine the audio-equipment brand, and the Alpine programming language (an experimental side project from a different decade) are all unrelated. If a search result mentions any of those, you are on the wrong tab.

Now the install. There are two paths, and most creators should pick path one.

Path one (recommended for almost every content creator): CDN script tag. Open your static-site generator's head partial. For Hugo, that is layouts/partials/head.html. For Eleventy, it is _includes/head.njk (or your equivalent). For Jekyll, _includes/head.html. For Astro, your base layout component. Paste one line into the <head>:

html
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>

The exact CDN URL above is taken verbatim from alpinejs.dev/start-here. The defer attribute tells the browser "download this in parallel with HTML parsing, run it after the DOM is ready." That means Alpine does not block first paint. Your Lighthouse score does not move. Your Largest Contentful Paint metric does not regress. The alpinejs.dev homepage also documents an alternate unpkg-hosted URL (<script src="//unpkg.com/alpinejs" defer></script>) which is functionally equivalent. Pick whichever CDN your other libraries already use to minimize DNS lookups.

A second consideration on the CDN install: pin the major version (@3.x.x) rather than tracking latest. The jsdelivr URL above pins to 3.14.1 (the version current in 2026 SERP captures). Pinning protects you from a major-version bump that breaks your existing markup. Alpine has been on the 3.x major line since 2021 and the maintainers prioritize backwards compatibility within a major, so pinning is a small belt-and-braces win rather than a constant maintenance burden.

Path two (for creators who already have a build step they like): npm. If your Astro site already runs a Vite build, or your Next.js site already runs the Next compiler, you can npm install alpinejs and import Alpine in your JavaScript entry. The install command is:

bash
npm install alpinejs

Then in your bundler entry (often main.js, script.js, or an Astro <script> block):

js
import Alpine from 'alpinejs'

window.Alpine = Alpine
Alpine.start()

The window.Alpine = Alpine line is the canonical pattern from alpinejs.dev. It exposes the Alpine global so you can call Alpine.store() and Alpine.data() from elsewhere on the page. The Alpine.start() call boots the runtime. Both lines are required when you install via npm. They are not required for the CDN path because the CDN bundle calls start() automatically.

Casey: I tried path two on my Astro site for one weekend because I thought it would feel more "professional." It did not. The CDN tag is the right answer for 95 percent of creators 95 percent of the time. Path two is only worth it if you are tree-shaking Alpine alongside your own JavaScript and you need to shave the last few kilobytes off your bundle.

For the deeper directive reference, see Alpine.js on Solomon Signal. For the gotchas around production usage, see Alpine.js best practices.

Your first Alpine interactivity in five minutes: the newsletter-signup form

Every content creator's day-one interactivity need is a newsletter signup form. Here it is in Alpine, end-to-end, on a static-site page, posting to ConvertKit. The pattern adapts to Mailchimp, Buttondown, EmailOctopus, MailerLite, or any provider with a form-submission endpoint.

Drop this block anywhere on a page in your blog. It works inside markdown (Hugo, Eleventy, Jekyll, Astro, MDX) because static-site generators pass HTML through unchanged.

html
<div x-data="{
  email: '',
  sent: false,
  loading: false,
  error: null,
  async submit() {
    this.loading = true
    this.error = null
    try {
      const res = await fetch('https://api.convertkit.com/v3/forms/YOUR_FORM_ID/subscribe', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ api_key: 'YOUR_PUBLIC_KEY', email: this.email })
      })
      if (!res.ok) throw new Error('subscribe failed')
      this.sent = true
    } catch (e) {
      this.error = e.message
    } finally {
      this.loading = false
    }
  }
}">
  <form x-on:submit.prevent="submit()" x-show="!sent">
    <label for="newsletter-email">Get the weekly post</label>
    <input
      id="newsletter-email"
      type="email"
      required
      x-model="email"
      placeholder="[email protected]"
    >
    <button type="submit" x-bind:disabled="loading">
      <span x-show="!loading">Subscribe</span>
      <span x-show="loading">Sending…</span>
    </button>
    <p x-show="error" x-text="error" style="color: crimson"></p>
  </form>

  <p x-show="sent">Thanks. Check your inbox for the confirmation email.</p>
</div>

Walk-through. The outer <div> carries x-data, which declares the component's reactive state: an email string, a sent boolean, a loading boolean, an error string, and a submit() method. Every directive inside the div has access to that state object.

The <form> carries x-on:submit.prevent="submit()". The x-on:submit part binds the form's submit event. The .prevent modifier calls event.preventDefault() for you. The submit() body fires the fetch. Alpine has a clean modifier syntax (.prevent, .stop, .outside, .window, .debounce.500ms, .throttle.500ms) that replaces a lot of the boilerplate every vanilla-JS signup form has to write.

The <input> carries x-model="email", which sets up two-way binding. When the user types, Alpine writes to email. When email changes, Alpine updates the input. No manual addEventListener('input') callback needed.

The <button> carries x-bind:disabled="loading". This is Alpine's way of binding an HTML attribute to a reactive value. When loading is true, the button is disabled. When false, it is enabled.

The <span> elements use x-show="!loading" and x-show="loading" to swap the button text between "Subscribe" and "Sending..." while the request is in flight. x-show toggles display: none via CSS, which is faster than x-if (which adds and removes the element entirely from the DOM).

The error paragraph uses x-text="error" to set its text content to the current error value. The success message uses a separate x-show that fires once sent becomes true.

Total: 30 lines of HTML, real form behavior, error handling, loading state, success state, no framework. The same pattern, with the fetch URL changed, posts to Mailchimp's embedded-form endpoint, Buttondown's API, or any provider's documented form post.

Casey: I rebuilt the signup form on five different sites with this exact pattern. The longest one took 12 minutes including swapping in the right form ID. The signup form is the interactive workhorse of every content business. Getting it down to one Alpine block that you paste once and reuse everywhere is a real productivity win.

Ten Alpine patterns every content creator should keep in their snippet library

These ten patterns cover roughly 95 percent of the interactive needs a content creator hits on a static site. Each is written against Alpine 3.x and uses only documented directives from alpinejs.dev. Paste them into your snippet library and reuse them post to post, course to course, newsletter to newsletter.

1. FAQ accordion

The pattern every long-form blog post and product page needs. Schema-friendly too (this structure pairs cleanly with FAQPage JSON-LD).

html
<div x-data="{ open: null }">
  <template x-for="(faq, idx) in [
    { q: 'Is Alpine.js dead in 2026?', a: 'No. Alpine 3.x is current, actively developed, and shipped inside Laravel Livewire and Tailwind UI.' },
    { q: 'Can I use Alpine inside MDX?', a: 'Yes. MDX passes raw HTML to the renderer, and Alpine attaches to plain HTML, so x-data blocks work inside .mdx files.' }
  ]" :key="idx">
    <div>
      <button x-on:click="open = open === idx ? null : idx" x-bind:aria-expanded="open === idx">
        <span x-text="faq.q"></span>
      </button>
      <div x-show="open === idx" x-transition x-text="faq.a"></div>
    </div>
  </template>
</div>

Note the <template x-for> pattern. Alpine renders lists inside a <template> tag the same way Vue does. The x-on:click toggles the index against the open state, which means only one FAQ is open at a time.

2. Dark-mode toggle (with no flash of light mode)

The trick is x-init, which runs before the rest of the directives bind. Set the class on <html> before paint, no FOUC.

html
<html x-data="{
  dark: localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
}" x-init="document.documentElement.classList.toggle('dark', dark)" x-effect="document.documentElement.classList.toggle('dark', dark); localStorage.theme = dark ? 'dark' : 'light'">

  <body>
    <button x-on:click="dark = !dark" x-text="dark ? 'Light' : 'Dark'"></button>
    <!-- rest of your page -->
  </body>
</html>

The x-effect runs whenever any reactive value it reads changes. When the user toggles, dark flips, x-effect re-runs, the class on <html> flips, and the new value is written to localStorage. Pair this with Tailwind's dark: variants or your own CSS that targets html.dark.

3. Lazy YouTube embed

A single YouTube embed costs roughly 500kb of JS. Lazy-loading the iframe via a thumbnail-first pattern is the biggest Lighthouse win on most tutorial blogs.

html
<div x-data="{ playing: false }" style="aspect-ratio: 16/9; position: relative;">
  <img
    x-show="!playing"
    src="https://i.ytimg.com/vi/VIDEO_ID/hqdefault.jpg"
    x-on:click="playing = true"
    alt="Video thumbnail"
    style="width: 100%; height: 100%; object-fit: cover; cursor: pointer;"
  >
  <iframe
    x-show="playing"
    src="https://www.youtube-nocookie.com/embed/VIDEO_ID?autoplay=1"
    frameborder="0"
    allow="autoplay; encrypted-media"
    allowfullscreen
    style="width: 100%; height: 100%;"
  ></iframe>
</div>

The iframe is not rendered until the user clicks. youtube-nocookie.com is YouTube's privacy-respecting embed domain, identical functionality, no third-party cookies set until playback. Pair with loading="lazy" if you ever do show the iframe by default.

For a creator who wants a hero gallery or a course-promo slide without shipping Slick or Swiper.

html
<div x-data="{
  idx: 0,
  slides: ['/img/slide-1.jpg', '/img/slide-2.jpg', '/img/slide-3.jpg'],
  next() { this.idx = (this.idx + 1) % this.slides.length },
  prev() { this.idx = (this.idx - 1 + this.slides.length) % this.slides.length }
}">
  <img x-bind:src="slides[idx]" alt="Slide" style="width: 100%;">
  <button x-on:click="prev()">Prev</button>
  <button x-on:click="next()">Next</button>
  <div>
    <template x-for="(s, i) in slides" :key="i">
      <button
        x-on:click="idx = i"
        x-bind:style="i === idx ? 'opacity: 1' : 'opacity: 0.4'"
      >•</button>
    </template>
  </div>
</div>

Sixteen lines for a working carousel with prev, next, and dot navigation. No external library.

The "copy this section's URL" pattern that every long-form post should have.

html
<button x-data="{ copied: false }" x-on:click="
  navigator.clipboard.writeText(window.location.href);
  copied = true;
  setTimeout(() => copied = false, 2000)
">
  <span x-show="!copied">Copy link</span>
  <span x-show="copied">Copied</span>
</button>

Five lines. The setTimeout resets the copied state after two seconds. No external dependency.

6. Course-content drawer

For a course platform that needs a slide-in table-of-contents drawer without committing to a SPA.

html
<div x-data="{ open: false }">
  <button x-on:click="open = true">Course contents</button>
<div x-show="open" x-transition.duration.300ms x-on:click.outside="open = false" x-on:keydown.escape.window="open = false" style="position: fixed; top: 0; right: 0; bottom: 0; width: 320px; background: white; box-shadow: -8px 0 24px rgba(0,0,0,0.1);" > <ul> <li><a href="#module-1">Module 1: Setup</a></li> <li><a href="#module-2">Module 2: First component</a></li> <li><a href="#module-3">Module 3: Theming</a></li> </ul> <button x-on:click="open = false">Close</button> </div> </div> ```

The .outside modifier on x-on:click closes the drawer when the user clicks anywhere outside it. The .escape.window modifier on x-on:keydown closes it on the escape key, listening on the window object. Accessibility-friendly with no manual event listener wiring.

7. Table-of-contents highlighter

Long-form posts need a "you are reading this section" indicator in the sidebar TOC. Pure Alpine plus the official @alpinejs/intersect plugin.

html
<!-- Add the intersect plugin once in your head, after the main Alpine CDN tag -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/[email protected]/dist/cdn.min.js"></script>

<aside x-data="{ active: null }">
  <a href="#intro" x-bind:class="active === 'intro' ? 'active' : ''">Intro</a>
  <a href="#install" x-bind:class="active === 'install' ? 'active' : ''">Install</a>
  <a href="#patterns" x-bind:class="active === 'patterns' ? 'active' : ''">Patterns</a>
</aside>

<main>
  <section id="intro" x-intersect="active = 'intro'">...</section>
  <section id="install" x-intersect="active = 'install'">...</section>
  <section id="patterns" x-intersect="active = 'patterns'">...</section>
</main>

x-intersect wires up an IntersectionObserver. When the section enters the viewport, Alpine writes to active. The sidebar links re-style themselves via x-bind:class.

8. Newsletter-signup form

Covered above in the "first interactivity in five minutes" section. The full code block belongs in your snippet library exactly as written. Swap the fetch URL for your provider.

9. Scroll-progress bar

The thin horizontal bar at the top of long-form posts that shows the reader how far they have scrolled.

html
<div x-data="{
  progress: 0,
  update() {
    const winScroll = document.documentElement.scrollTop || document.body.scrollTop
    const height = document.documentElement.scrollHeight - document.documentElement.clientHeight
    this.progress = (winScroll / height) * 100
  }
}" x-init="update(); window.addEventListener('scroll', () => update())">
  <div
    x-bind:style="`width: ${progress}%`"
    style="position: fixed; top: 0; left: 0; height: 4px; background: tomato; transition: width 0.1s;"
  ></div>
</div>

Twelve lines. No external library. The x-init directive wires up the scroll listener at boot.

10. Code-block copy button

For a tutorial blog where every code block deserves a "copy" button.

html
<div x-data="{ copied: false }" style="position: relative;">
  <pre><code>const greeting = 'hello, creator'
console.log(greeting)</code></pre>
  <button
    x-on:click="navigator.clipboard.writeText($el.previousElementSibling.innerText); copied = true; setTimeout(() => copied = false, 2000)"
    style="position: absolute; top: 8px; right: 8px;"
  >
    <span x-show="!copied">Copy</span>
    <span x-show="copied">Copied</span>
  </button>
</div>

$el is the Alpine magic property that references the current element. Walking to previousElementSibling.innerText grabs the <pre> content. Adapt the DOM walk if your structure differs.

Ten patterns, every one production-ready, every one using only directives documented at alpinejs.dev. Save this file, reuse the patterns across every blog post, every course page, every newsletter landing you ship from now on.

Integrate Alpine.js with your static-site stack

Most creator stacks are one of five: Hugo, Eleventy, Astro, Jekyll, or an MDX-based pipeline (Next.js with MDX, Astro Content Collections, Docusaurus, Nextra). Alpine drops into all five with one line in the head partial. Here is each one, worked.

Hugo

Open layouts/partials/head.html. Paste at the bottom of the <head> block:

html
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>

Hugo's markdown parser passes raw HTML through to the rendered output by default. That means you can write <div x-data="{ open: false }">...</div> blocks directly inside your content/posts/*.md files. If a future version of your Hugo theme uses Goldmark with unsafe: false for security, you may need to add the following to your config.toml:

toml
[markup.goldmark.renderer]
unsafe = true

This allows raw HTML inside markdown. Without it, Hugo strips the <div x-data> block before render. The unsafe = true setting is the documented Hugo path for embedding HTML in markdown.

Eleventy

Open your base layout, often _includes/base.njk or _includes/layouts/base.njk. Paste the same script tag inside the <head> block:

html
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>

Eleventy passes HTML through markdown by default via the markdown-it engine. No config change needed. Your posts/*.md files can contain Alpine blocks. If you have configured Eleventy with a stricter markdown setup, ensure html: true is set on the markdown-it instance in your .eleventy.js:

js
const markdownIt = require('markdown-it')
const md = markdownIt({ html: true })
eleventyConfig.setLibrary('md', md)

Astro

Astro is the trickiest of the five because Astro's <script> tag handling is opinionated. By default Astro bundles, processes, and tree-shakes any <script> tag in a component. For Alpine via CDN, you want the script to pass through untouched. The fix is the is:inline directive:

astro
---
// src/layouts/Base.astro
---
<html lang="en">
  <head>
    <script is:inline defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
  </head>
  <body>
    <slot />
  </body>
</html>

The is:inline directive tells Astro "do not bundle or process this script tag, render it as-is." That is what you want for a CDN-loaded third-party runtime. Without is:inline, Astro tries to bundle the script and the defer behavior gets weird.

Astro markdown (.md files) passes HTML through by default, so <div x-data> blocks work. For Astro Content Collections that use MDX (.mdx files), the same rule applies: raw HTML works, Alpine attaches.

Jekyll

Open _includes/head.html. Paste:

html
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>

Jekyll uses Kramdown by default, which permits raw HTML inside markdown out of the box. No config change needed. Your _posts/*.md files can contain Alpine blocks immediately.

MDX (Next.js, Docusaurus, Nextra, Astro Content Collections)

MDX is markdown with JSX. Raw HTML works inside MDX the same way it works inside plain markdown. Drop the Alpine CDN tag into your global layout's head element. Inside an .mdx file, write Alpine blocks as plain HTML:

mdx
# My post

Some prose.

<div x-data="{ open: false }">
  <button onClick="open = !open">Toggle</button>
  <p x-show="open">Hidden content</p>
</div>

More prose.

One caveat: MDX's JSX parser treats some attributes as JSX props. The x-on:click syntax with a colon may confuse certain MDX setups. In those cases, use the at-symbol shorthand (@click) instead, which Alpine supports as an alias and which MDX is happy with.

Casey: I run Alpine on all five stacks in production. The Astro is:inline trick is the only gotcha worth memorizing. The other four (Hugo, Eleventy, Jekyll, MDX) are paste-the-script-tag and you are done.

When Alpine.js is not the right pick

Honest section. Alpine wins on most creator interactivity, but not on everything. Pick a different tool in these three scenarios.

Data-heavy dashboards

If your interactive surface is a 500-row sortable filterable table, a Kanban board, a charting dashboard, or a real-time admin panel, Alpine is the wrong choice. The directive-on-attribute model gets ugly past a certain complexity threshold. Vue, React, or Svelte will give you better ergonomics, better DevTools, and better performance once you cross the 50-component mark. Alpine is designed for "sprinkles," not for a SPA. The Alpine maintainers say this themselves on alpinejs.dev: "minimal tool for composing behavior."

Full SPA with client-side routing

If your product is a single-page app where the URL changes without a full page reload, where you have nested routes, dynamic route parameters, and client-side data fetching coordinated across pages, Alpine is not the answer. There is no Alpine Router. There is no Alpine equivalent of Next.js, Remix, Nuxt, or SvelteKit. Static-site generators plus Alpine cover content sites well. They do not cover SPA-style products. For a course platform with deep interactivity (interactive code playgrounds, drag-and-drop lesson reordering, real-time progress sync) you will likely want Astro + React islands, Next.js, Remix, or SvelteKit.

Complex shared state across distant components

Alpine has Alpine.store() for global state, and it works fine for theme state or auth state. But if you need redux-style time-travel debugging, complex action middleware, or normalized cache management across hundreds of components, Alpine's state model will feel thin. That is by design. Reach for Pinia (Vue), Zustand or Redux (React), or a Svelte store. Alpine wins on simplicity, and the simplicity is the constraint.

Casey: I keep Alpine on every content site I run. I keep React (via Astro islands) on my course site's interactive code playground because the playground is real software, not sprinkles. Two tools, two jobs.

SEO, performance, and the defer trick: why Alpine doesn't hurt your Core Web Vitals

Lighthouse and Core Web Vitals are not vanity metrics for a content creator. Google uses them as a ranking input. A regressed Largest Contentful Paint or Cumulative Layout Shift score can quietly cost organic traffic. Here is exactly what Alpine does to your Core Web Vitals on a static-site blog, and how to keep it optimal.

The first lever is defer. The official CDN install at alpinejs.dev/start-here writes <script defer src="...">. The defer attribute is a documented HTML attribute on <script> tags. It tells the browser two things: download the script in parallel with HTML parsing (no blocking), and execute the script after the DOM is parsed but before the DOMContentLoaded event fires. That means Alpine does not block your First Contentful Paint or your Largest Contentful Paint. The Alpine runtime loads in parallel with your images and your CSS. If you measure your Lighthouse score before and after dropping in the CDN tag, the difference on a static site is single-digit milliseconds.

The second lever is x-cloak. Alpine binds directives after the DOM is ready, which means for a brief window your page renders with raw, unprocessed Alpine attributes visible. An accordion's hidden content might flash visible for 100ms before x-show kicks in. The fix is the x-cloak directive plus one line of CSS:

html
<style>
  [x-cloak] { display: none !important; }
</style>

<div x-data="{ open: false }" x-cloak>
  <!-- content -->
</div>

The CSS hides any element with the x-cloak attribute. Alpine removes the x-cloak attribute on every element once the directives are bound. The result: no flash of unprocessed content. Add the two lines (<style> and one x-cloak per top-level Alpine block) to your base layout's head and the FOUC disappears.

The third lever is bundle size. Alpine 3.x is roughly 15kb minified and gzipped. Compare to React + React-DOM at ~45kb gzipped, Vue 3 at ~32kb gzipped, Svelte (at the per-component bundle) variable but commonly 5-10kb per component. Alpine pays its weight in features and never grows from your usage. You write more Alpine, the bundle stays the same size. This is not true of React or Vue, where every component you author adds to the bundle.

The fourth lever is async fetches. Your x-on:submit.prevent="submit()" newsletter form fetches happen entirely client-side, after first paint. The form HTML renders immediately with the page. The submit handler waits for user interaction. Nothing about the form blocks initial render.

For long-tail performance on a Hugo or Astro blog with thirty Alpine blocks across a page, profile the runtime. Alpine's per-block startup cost is tiny (microseconds) but it compounds. If you have a page with thirty separate x-data blocks, consider consolidating shared state into a single Alpine.store() global rather than thirty independent components.

Casey: my Hugo blog ships Alpine alongside roughly 200kb of total page weight (HTML + CSS + images + Alpine). Lighthouse Performance on a fresh build is 99. The 15kb of Alpine does not show up in the audit. The defer is doing its job.

Frequently asked questions

Is Alpine.js dead in 2026?

No. Alpine.js is alive and actively developed. The current major is 3.x, the maintainer is Caleb Porzio, the repository at github.com/alpinejs/alpine shows active commits and releases. The framework is shipped inside Laravel Livewire, which is one of the most-deployed PHP stacks in the world, and it is referenced in Tailwind UI's interactive component patterns. The combination of an active maintainer, a stable major-version line since 2021, and embedding in a high-volume production framework means Alpine is not going anywhere. If a tutorial you find online claims Alpine is dead, check the date of the post.

Alpine vs HTMX for content sites?

HTMX and Alpine are complementary, not competitive. HTMX is "send fragments of HTML from the server in response to user actions, swap them into the page." Alpine is "manage client-side state and interactivity inline in your HTML." Many creators run both: HTMX for server-driven swaps (search-as-you-type backed by a real endpoint, comment loading, paginated lists) and Alpine for purely client-side interactivity (dark-mode toggle, accordion, dropdown). The two libraries explicitly support each other and there is no architectural conflict. If your blog is entirely static and you do not run a backend, you do not need HTMX, just Alpine. If your blog is static but has a comment endpoint and a search endpoint, run both.

Can I use Alpine inside MDX or markdown?

Yes. Markdown engines (Goldmark for Hugo, Kramdown for Jekyll, markdown-it for Eleventy, remark for Astro and MDX) pass raw HTML through to the rendered output. Drop a <div x-data="{ open: false }">...</div> block directly into your .md or .mdx file and Alpine attaches at runtime. The one MDX-specific gotcha is the colon in x-on:click. MDX's JSX parser sometimes objects. Use the at-symbol shorthand (@click) as an alias and the issue goes away. Hugo with Goldmark requires unsafe = true in your config to allow raw HTML inside markdown. Every other major static-site generator allows it by default.

Is Alpine production-ready for a paid course site?

Yes. Alpine is used in Laravel Livewire (production-grade Laravel apps), in Tailwind UI (a paid component library), and in countless creator and indie products. The framework is MIT-licensed (per github.com/alpinejs/alpine), free for commercial use, no royalties, no per-seat license. Production-ready means "actively maintained, deployed at scale, predictable behavior." Alpine meets all three. The honest constraint is that Alpine is best for sprinkle-style interactivity, not for SPA-style products. A course site whose interactivity is "play a video, mark a lesson complete, toggle a sidebar, expand FAQs" is well-served by Alpine. A course site whose interactivity is "interactive code playground with real-time output, drag-and-drop lesson reordering" likely needs a real framework alongside Alpine.

Best Alpine + Astro integration pattern?

Drop the CDN script tag inside your base layout component with the is:inline directive. Astro processes <script> tags by default, which interferes with the defer behavior of a third-party runtime. The is:inline directive tells Astro "render this script tag as-is, do not bundle." The pattern is:

astro
<script is:inline defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>

That goes in your <head> element in the base layout. Once the runtime is loaded, Alpine attaches to any x-data block in your Astro components, your .md content, and your .mdx content. For islands of heavier interactivity, you can mix Astro's React/Vue/Svelte islands with Alpine on the same page. Alpine handles the lightweight surfaces, the framework islands handle the heavy ones.

Will Alpine slow down my blog's Lighthouse score?

No, if you install it correctly. The official install includes the defer attribute, which means the runtime downloads in parallel with HTML parsing and executes after the DOM is ready. Alpine does not block First Contentful Paint or Largest Contentful Paint. The runtime is roughly 15kb minified and gzipped, which is smaller than a single hero image on most blogs. Add the x-cloak pattern (a one-line CSS rule plus the x-cloak attribute on top-level blocks) to eliminate the brief flash of unprocessed content during initial bind. On a typical Hugo or Astro blog, the Lighthouse Performance score difference before and after adding Alpine is within the measurement noise floor.

Where to go next

If you have not picked a package manager yet, the persona-sibling tutorial for content creators is pnpm for content creators, which covers disk-pressure relief and creator-monorepo patterns. If you are running a React-end-to-end stack and want a full component library rather than Alpine's sprinkle model, see Chakra UI for content creators. For the deeper directive reference, see Alpine.js on Solomon Signal. For the gotchas, see Alpine.js best practices. For the head-to-head against the other lightweight frameworks (Petite Vue, Stimulus, htmx), see alternatives to Alpine.js. Authoritative reference for the directive surface and the install commands is the official alpinejs.dev and the source at github.com/alpinejs/alpine.

Casey: I started with the newsletter-signup pattern, then added the FAQ accordion, then the dark-mode toggle, then the lazy YouTube embed. Four patterns covered 80 percent of the interactivity I needed across four sites. The other six patterns in this guide live in my snippet library for the day a post asks for them. The whole stack runs on one 15kb CDN tag. That is the trade Alpine offers a content creator. Take it.

Read the full Alpine.js for Content Creators: No-Build JS Interactivity for Hugo/Astro/Eleventy Blogs (2026) review

Alpine.js for Content Creators: No-Build JS Interactivity for Hugo/Astro/Eleventy Blogs (2026) Use Cases FAQ

Common questions about applying Alpine.js for Content Creators: No-Build JS Interactivity for Hugo/Astro/Eleventy Blogs (2026) to real workflows

No. Alpine 3.x is current, actively developed by Caleb Porzio and contributors, and shipped inside Laravel Livewire and Tailwind UI's interactive component patterns. The framework has been on the 3.x major line since 2021 with a strong backwards-compatibility track record. The repository at github.com/alpinejs/alpine shows active commits and releases. If a tutorial claims Alpine is dead, check the post's publication date.
HTMX and Alpine are complementary, not competitive. HTMX swaps server-sent HTML fragments into the page in response to user actions. Alpine manages client-side state and interactivity inline. Many creators run both: HTMX for server-driven swaps (search, comments, paginated lists) and Alpine for purely client-side interactivity (dark-mode toggle, accordion, dropdown). If your blog is entirely static with no backend, you only need Alpine.
Yes. Markdown engines like Goldmark (Hugo), Kramdown (Jekyll), markdown-it (Eleventy), and remark (Astro/MDX) pass raw HTML through to the rendered output. Drop an x-data block directly into a .md or .mdx file and Alpine attaches at runtime. Hugo requires unsafe = true in the Goldmark renderer config. For MDX, use the @click shorthand instead of x-on:click if the JSX parser complains about the colon.
Yes. Alpine is used in Laravel Livewire (production Laravel apps), in Tailwind UI (a paid component library), and in countless indie products. It is MIT-licensed per github.com/alpinejs/alpine, free for commercial use. The honest constraint is that Alpine fits sprinkle-style interactivity (signup form, video toggle, FAQ accordion, dark mode), not SPA-style products. A course site with an interactive code playground or drag-and-drop lesson reordering likely needs Alpine alongside a framework like React or Vue, not Alpine alone.
Drop the CDN script tag inside your base layout's head with Astro's is:inline directive: <script is:inline defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>. The is:inline directive prevents Astro from bundling the third-party runtime, which preserves the defer behavior. Once loaded, Alpine attaches to any x-data block in Astro components, .md content, and .mdx content. For heavier interactivity on the same page, mix Astro's framework islands (React, Vue, Svelte) with Alpine for the lightweight surfaces.
No, when installed correctly. The official install includes the defer attribute, so the 15kb runtime downloads in parallel with HTML parsing and executes after the DOM is ready. Alpine does not block First Contentful Paint or Largest Contentful Paint. Add the x-cloak pattern (one CSS rule plus x-cloak on top-level blocks) to eliminate the brief flash of unprocessed content during initial directive binding. On a typical Hugo or Astro blog the Lighthouse Performance score difference before and after adding Alpine is within the measurement noise floor.