Use case

Alpine.js for Freelancers: No-Build Client Interactivity That Hands Off Cleanly (2026)

Alpine.js for freelance client work in 2026 - sprinkle interactivity on client sites without a build pipeline. 10 patterns, handoff template, and pricing framework inside.

35 min read·Updated 2026

Alpine.js is the JavaScript framework for freelance client work where you need interactivity on a client's site but cannot commit the client to a React or Vue build pipeline. Drop a single <script defer> CDN tag in the client's existing site head, write reactive state and event handlers directly in HTML attributes, and ship a working signup form or FAQ accordion in fifteen minutes. The wedge for a freelancer is the handoff. Your client's marketing person can copy-paste an Alpine x-data block into a new landing page in their CMS without breaking the site. The client's IT team has no Node version to debate, no bundler to maintain, no package.json to keep in sync. Alpine 3.x is current in 2026, MIT-licensed, roughly 15kb minified-and-gzipped, and learned in an hour because the entire API is fifteen directives that read like HTML attributes. This is the freelancer playbook for Alpine: when it wins for client work, the ten client-deliverable patterns to keep on file, how to wire it into a Hugo, Eleventy, Astro, Jekyll, or WordPress site, the HANDOFF.md template, and the pricing patterns that turn an Alpine sprinkle into a clean billable line item.

Alpine.js for freelancers: handoff advantages at a glance

The table below names the four freelancer-specific advantages Alpine.js holds over React, Vue, or jQuery for client work. Behavior reflects Alpine 3.x documented at alpinejs.dev as of 2026.

Freelancer concernReact / Vue answerAlpine.js answerWhy it matters on a client project
Build stepWebpack / Vite / esbuild required, npm install, package.json, dist folder, deploy pipelineNone. Drop a <script defer src="...cdn.min.js"> tag in the client's existing head. No bundler.The freelancer skips the entire "what is your client's Node version" conversation. The client's existing static-site / WordPress / CMS deploy works unchanged.
Runtime sizeReact + ReactDOM ~45kb gzipped before your code; Vue ~32kb gzipped~15kb minified-and-gzipped per the Alpine 3.x CDN bundleSmaller than a single hero image on most client sites. No conversation with the client's IT about performance review.
LicenseMIT (React, Vue both MIT)MIT per github.com/alpinejs/alpineClean IP, commercial-friendly, no royalty, no per-seat license. Drop into any commercial client contract.
Client can edit laterRequires JSX + build knowledge + the client running npm installPlain HTML attributes (x-data, x-on:click, x-show); the client's marketing person can copy-paste blocksThe client edits an Alpine FAQ accordion to add a question without touching JavaScript. You hand off cleaner and they call you back less.
Multi-client workflowFive node_modules/ directories, five build pipelinesOne Alpine boot file template, one shared snippets library, drop in per clientA freelancer with 3-12 active client repos saves the build-pipeline maintenance tax on every project.
Pricing line item"Set up build pipeline" + "Configure Webpack" + "Build CI" + "Write components""Add Alpine.js interactivity layer (1-3h)" + "Per-component implementation (30-60min each)"The Alpine line item is concrete, billable, and small enough for a fixed-price quote without scope creep.

Casey here. I am the freelancer this page is written for. I run a one-person studio that ships marketing sites, landing pages, micro-features, and the occasional CMS theme to a rotating roster of about eight active clients. Five of those clients run WordPress with a custom child theme. Two run Eleventy that I built. One runs an old Hugo site I inherited. For two years I tried to justify React on every project that needed more than a hover state. Every time I finished, I had to explain build steps to a client's IT person who only wanted to know if the contact form worked. The week I picked Alpine.js as my default interactivity layer for client work, the conversations got shorter, the handoff got cleaner, and the invoices got paid faster. The rest of this page is what I wish someone had written when I was looking for "alpine.js tutorial for freelancers" and found only docs aimed at developers debating which lightweight framework to learn next.

Why freelancers pick Alpine for client work

Most Alpine tutorials are written for the engineer learning a lightweight framework. You are probably not that engineer. You are a freelancer or contractor or agency-of-one who needs a JavaScript interactivity layer that does not become a maintenance liability the day after the project closes. Alpine wins on four freelancer-specific stakes that the generic tutorials never name.

First, no build step means no Node version debate with the client. The official install at alpinejs.dev is one line of HTML you paste into the client's existing <head> block: <script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>. There is no npm install, no package.json to maintain inside the client's repo, no node_modules/ directory to commit-or-not, no Vite or Webpack config the next developer has to read. When you hand the project off, the client's existing deploy works unchanged. If the client moves from Cloudways to WP Engine next quarter, the script tag travels with the theme files and keeps working. The freelancer math: every project I do not commit to a build pipeline saves me roughly two hours of "explain Node to the client's marketing team" plus an unknown number of "the build is broken on the client's deploy" emails six months later.

Second, the client can edit Alpine HTML themselves. This is the wedge a developer-targeted Alpine tutorial cannot see. The client's marketing person opens the FAQ section of a landing page, sees <div x-data="{ open: false }"> followed by <button x-on:click="open = !open">, and pattern-matches it to "the thing I expand and collapse." They can copy that block, paste it on a new landing page, change the question text, and ship without your involvement. Try that with a React FAQ component. The client cannot copy a <FAQ items={[...]} /> JSX block into their CMS because it never renders without the build. With Alpine the HTML attributes are the source of truth, and the source of truth lives where the client edits content. You hand off cleaner. They call you back less. The recurring-tweaks revenue you lose is more than offset by the referral-and-retention value of a client who feels in control of their own site.

Third, fifteen kilobytes is below the conversation threshold. Alpine 3.x ships at roughly 15kb minified and gzipped. That is smaller than a single hero image on most client sites. When the client's IT person, or the client's agency-of-record, or the client's "my brother-in-law knows computers" reviewer asks about performance, the answer is "we ship 15kb of JavaScript with defer, it loads in parallel with the page, it does not block first paint, here is the Lighthouse score." The conversation ends in 90 seconds. Try that with React + React-DOM at 45kb gzipped before you write a line of component code. The performance review with the client's IT becomes a project. With Alpine, it becomes a footnote.

Fourth, the API surface fits in a single conversation. The Alpine docs list fifteen directives total: 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 a freelancer understands x-data (declares reactive state) and x-on (binds events), they can write 80 percent of the Alpine they will ever need on a client project. The official alpinejs.dev/start-here walkthrough is about a one-hour read. Compare to React: hooks, JSX, virtual DOM, reconciler, refs, effects, suspense, server components, the framework's annual evolution. Picking Alpine as a freelancer is picking the tool whose entire surface fits in your head AND in the head of the next freelancer the client hires after you.

Casey: my single highest-value freelance decision in 2025 was making Alpine.js my default JavaScript interactivity layer for any client project that does not specifically require a React or Vue build. The clients who hire me for a marketing site, a landing page, or a micro-feature get Alpine. The clients who hire me for "rebuild our dashboard product" get the real framework conversation. Two tools, two jobs, fewer arguments.

Install Alpine on a client site in five minutes

Real workflow. You have just been hired to add interactivity to a client's existing site. The site is one of: WordPress with a custom child theme, an Eleventy build, a Hugo blog, an Astro marketing site, a Jekyll legacy site, or a hand-rolled static HTML build. You need a signup form, an FAQ accordion, or a dark-mode toggle live by end-of-week. The Alpine install is the first 90 seconds of the engagement.

Quick disambiguation note first, because freelancers googling "alpine for client site" sometimes land on the wrong tab. Alpine.js is the JavaScript framework at alpinejs.dev, MIT-licensed, maintained by Caleb Porzio, current at version 3.x in 2026. Alpine Linux is a separate project at alpinelinux.org, a security-oriented small-footprint Linux distribution used inside Docker images. The ski resort, the car-audio brand, and the experimental Alpine programming language are all unrelated. If a search result mentions Linux, kernels, Docker, or skiing, you are on the wrong tab.

Open the client's site source. Find the file that controls the <head> element. For WordPress, that is your child theme's header.php or functions.php with a wp_head hook. For Hugo, layouts/partials/head.html. For Eleventy, _includes/head.njk or your project's equivalent. For Astro, the base layout component. For Jekyll, _includes/head.html. For a hand-rolled HTML site, the <head> block in each page (or the shared partial if the site uses includes).

Paste one line into the <head> block:

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 is the documented HTML attribute that tells the browser two things at once: download this script in parallel with the rest of HTML parsing (no blocking), and execute it after the DOM is ready but before DOMContentLoaded fires. That means Alpine does not block first paint. Your client's Lighthouse score does not regress. The Largest Contentful Paint metric does not change. The @3 in the URL pins to the Alpine 3 major version, which the maintainers have kept stable since 2021. Pinning to a major (rather than tracking latest) is the freelancer-friendly default because it protects the client from a major-version bump that breaks their existing markup after you hand off.

Now write the first Alpine block to confirm the install works. Drop this anywhere in the body of any client page:

html
<div x-data="{ count: 0 }">
  <button x-on:click="count++">Clicked <span x-text="count"></span> times</button>
</div>

Save. Reload the client's page in your browser. Click the button. The counter increments. The install is verified. Total elapsed time: three minutes if the client's repo is already on your machine.

A few install-time decisions that matter on a client project.

Use jsdelivr or unpkg, not your own CDN. The Alpine docs ship both. jsdelivr is the default at cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js. unpkg is unpkg.com/[email protected]/dist/cdn.min.js. Both are functionally equivalent. Pick whichever CDN your other libraries already use to minimize DNS lookups on the client's site. Do not host the file on the client's own CDN unless the client has a documented policy that prohibits third-party CDNs (some healthcare and finance clients do). If they do, download the cdn.min.js file, drop it in the client's assets folder, and point the src at the local path. Same result.

Always include defer on the script tag. Without defer, Alpine attaches before the DOM is ready and your x-data blocks throw "element not found" errors. Every Alpine guide on the web shows defer. Some clients with legacy CMSes have plugins that strip the defer attribute from <script> tags. If your Alpine breaks on the client's site but works on your local copy, check whether a security plugin or a JS-minifier plugin is rewriting the tag.

Add the x-cloak CSS rule to the client's stylesheet. Alpine binds directives after the DOM is parsed, which means for a brief window the page renders with raw, unprocessed Alpine attributes visible. An accordion's hidden content can flash visible for 100 milliseconds before x-show kicks in. The fix is one line of CSS plus the x-cloak attribute on every top-level Alpine block:

css
[x-cloak] { display: none !important; }
html
<div x-data="{ open: false }" x-cloak>
  <!-- content -->
</div>

Alpine removes the x-cloak attribute the moment the directives bind. The flash of unprocessed content disappears. Add the CSS rule once to the client's main stylesheet and the x-cloak attribute on every top-level block. Five seconds per page, no flash on first paint.

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

Ten client-deliverable Alpine patterns every freelancer keeps on file

These ten patterns cover roughly 95 percent of the interactivity needs a freelancer hits on client work. Each is written against Alpine 3.x and uses only documented directives from alpinejs.dev. Save them to your snippet library. Reuse them client to client.

1. Newsletter signup form posting to the client's email service

Almost every client wants a newsletter signup. Here it is end-to-end, posting to ConvertKit (swap the URL for Mailchimp, Buttondown, MailerLite, EmailOctopus, or whatever the client uses).

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/CLIENT_FORM_ID/subscribe', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ api_key: 'CLIENT_PUBLIC_KEY', email: this.email })
      })
      if (!res.ok) throw new Error('Subscribe failed. Please try again.')
      this.sent = true
    } catch (e) {
      this.error = e.message
    } finally {
      this.loading = false
    }
  }
}" x-cloak>
  <form x-on:submit.prevent="submit()" x-show="!sent">
    <label for="newsletter-email">Get the newsletter</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>

Twenty-five lines. Loading state, error state, success state, no jQuery, no dependency on the client's email service's WordPress plugin. The x-on:submit.prevent modifier calls event.preventDefault() for you. The x-model two-way-binds the email field to the reactive state. The x-bind:disabled="loading" disables the button while the request is in flight. Swap the fetch URL for the client's actual email service endpoint and you are done.

2. FAQ accordion (FAQPage-schema-friendly)

Every client product page wants an FAQ block. Here is the accordion using only documented Alpine 3.x directives.

html
<div x-data="{ open: null }" x-cloak>
  <template x-for="(faq, idx) in [
    { q: 'How long does delivery take?', a: 'Two to four business days for orders within the continental US.' },
    { q: 'What is your return policy?', a: 'Thirty days from receipt for unused items in original packaging.' }
  ]" :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>

The <template x-for> pattern is how Alpine renders lists, identical to Vue. The x-on:click toggles the index against the open state, which means only one FAQ is open at a time (accordion behavior). The x-transition directive applies a default fade transition. The structure pairs cleanly with FAQPage JSON-LD if the client wants rich-result eligibility in Google Search.

3. Dark-mode toggle for the client's brand

A common ask on client product sites and creator-brand sites. The trick is x-init running before the rest of the directives bind, plus x-effect to keep the class and localStorage in sync.

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 mode' : 'Dark mode'"></button>
  </body>
</html>

Pair with Tailwind's dark: variants or any CSS that targets html.dark. The x-init runs once before directives bind, setting the class on <html> before paint. The x-effect re-runs whenever dark changes, syncing the class and localStorage. No flash of light mode on page load.

When the client wants a hero gallery without shipping 50-200kb of carousel library.

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 }
}" x-cloak>
  <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, prev / next / dot navigation, no third-party library. The freelancer bills the carousel as a 30-60 minute line item instead of "install and configure Swiper plus theme its 200kb runtime."

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 state after two seconds. No external dependency.

6. Contact form posting to Formspree

When the client does not have a backend, Formspree (or Basin, Getform, FormSubmit) handles the email forwarding. Alpine handles the UX.

html
<div x-data="{
  data: { name: '', email: '', message: '' },
  sent: false,
  loading: false,
  error: null,
  async submit() {
    this.loading = true
    this.error = null
    try {
      const res = await fetch('https://formspree.io/f/CLIENT_FORMSPREE_ID', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
        body: JSON.stringify(this.data)
      })
      if (!res.ok) throw new Error('Submission failed. Please email us directly.')
      this.sent = true
    } catch (e) {
      this.error = e.message
    } finally {
      this.loading = false
    }
  }
}" x-cloak>
  <form x-on:submit.prevent="submit()" x-show="!sent">
    <input type="text" x-model="data.name" placeholder="Your name" required>
    <input type="email" x-model="data.email" placeholder="Your email" required>
    <textarea x-model="data.message" placeholder="Message" required></textarea>
    <button type="submit" x-bind:disabled="loading">
      <span x-show="!loading">Send</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. We will be in touch within one business day.</p>
</div>

Swap the Formspree ID for the client's actual endpoint. Same structure as the newsletter form. Reuse the muscle memory.

7. Lazy YouTube embed

A single YouTube embed costs roughly 500kb of JavaScript before the user has decided to play. Lazy-loading the iframe behind a thumbnail-first pattern is the single biggest Lighthouse win on most client tutorial / video-heavy pages.

html
<div x-data="{ playing: false }" style="aspect-ratio: 16/9; position: relative;" x-cloak>
  <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 loaded until the user clicks. youtube-nocookie.com is YouTube's privacy-respecting embed domain (no third-party cookies until playback).

8. Course-content drawer for the client's course or membership product

Slide-in panel for a table-of-contents or product-detail drawer, with click-outside-to-close and escape-key-to-close.

html
<div x-data="{ open: false }" x-cloak>
  <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>
    </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 the drawer on the escape key, with the event listener attached to the window object. Accessibility-friendly defaults, no manual event-listener wiring.

9. Scroll-progress bar for long-form client content

A thin horizontal bar at the top of long-form articles or product pages that shows how far the reader has 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())" x-cloak>
  <div
    x-bind:style="`width: ${progress}%`"
    style="position: fixed; top: 0; left: 0; height: 4px; background: var(--brand); transition: width 0.1s;"
  ></div>
</div>

Twelve lines. The x-init wires up the scroll listener once at boot. Style the bar with the client's brand color via CSS custom property.

10. Table-of-contents highlighter (current section in sidebar)

For long-form client content with a sidebar TOC that highlights the section currently in view. Uses the @alpinejs/intersect plugin, the official Alpine plugin that wraps IntersectionObserver.

html
<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="#features" x-bind:class="active === 'features' ? 'active' : ''">Features</a>
  <a href="#pricing" x-bind:class="active === 'pricing' ? 'active' : ''">Pricing</a>
</aside>

<main>
  <section id="intro" x-intersect="active = 'intro'"><!-- ... --></section>
  <section id="features" x-intersect="active = 'features'"><!-- ... --></section>
  <section id="pricing" x-intersect="active = 'pricing'"><!-- ... --></section>
</main>

Add the intersect plugin tag after the main Alpine CDN tag. x-intersect fires when the section enters the viewport. The sidebar links restyle themselves via x-bind:class.

Ten patterns, each production-ready, each using only documented Alpine 3.x directives, each small enough for a freelancer to drop into a client repo as a fixed-price line item. Save this section. Reuse the patterns across every client engagement from now on.

Multi-client config strategy: one boot file per repo, shared snippets across clients

The question every freelancer eventually asks: do I keep Alpine inline in every client repo (duplicate the install per project), or do I maintain a personal "alpine-snippets" library that all my clients pull from? The honest answer is some of column A and some of column B. Here is the pattern that works.

Per-client repo. Every client gets the Alpine CDN tag pasted into their own site's head. Every client's repo contains their own copy of the Alpine blocks (newsletter form, FAQ accordion, dark-mode toggle). The duplication is fine. Alpine blocks are HTML attributes; they belong with the markup. Do not extract them into a shared package that the client cannot edit.

Personal snippets library. You keep a personal ~/snippets/alpine/ folder on your laptop with one file per pattern: newsletter-signup.html, faq-accordion.html, dark-mode-toggle.html, image-carousel.html, contact-form-formspree.html, lazy-youtube.html, course-drawer.html, scroll-progress.html, toc-highlighter.html, copy-link.html. When you start a new client engagement, you copy from the snippets folder, paste into the client's repo, swap the configuration (form IDs, brand colors, content), and commit. The library is your IP, the per-client implementation is the client's deliverable. Each pattern stays current in your library when you improve it on a future client, and you propagate the improvement to other clients on the next engagement.

A modest shared utility. Some freelancers maintain a single alpine-utils.js file in their personal snippets folder with one or two shared helpers (a formatDate() function, a slugify() function, a debounced throttle() helper). When a client project needs the helper, you copy that one file in alongside the Alpine CDN tag. It is small, it is plain JavaScript, and it never grows into a framework. If the helper file ever crosses 200 lines, split it.

Configuration via Alpine.data() for repeated patterns. When the same Alpine block appears multiple times on a single client site (the contact form on the contact page, the contact form on the footer, the contact form on the pricing page), use Alpine.data() to define a reusable component:

html
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
<script>
  document.addEventListener('alpine:init', () => {
    Alpine.data('contactForm', () => ({
      data: { name: '', email: '', message: '' },
      sent: false,
      loading: false,
      error: null,
      async submit() {
        this.loading = true
        this.error = null
        try {
          const res = await fetch('https://formspree.io/f/CLIENT_ID', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
            body: JSON.stringify(this.data)
          })
          if (!res.ok) throw new Error('Submission failed.')
          this.sent = true
        } catch (e) {
          this.error = e.message
        } finally {
          this.loading = false
        }
      }
    }))
  })
</script>

Then in the markup you call the data block by name:

html
<form x-data="contactForm" x-on:submit.prevent="submit()">
  <!-- inputs as before -->
</form>

Alpine.data() is documented at alpinejs.dev. The alpine:init event fires once before Alpine starts processing the page. Define your shared component definitions inside that listener. The trade is a few extra lines at the top of the client's site script in exchange for not duplicating the contact-form state object six times.

Alpine.store() for cross-block state. When you need shared state across distant components on the same page (a global theme state, an auth state, a cart count), Alpine.store() is the documented Alpine 3.x global store. Define stores once inside alpine:init:

html
<script>
  document.addEventListener('alpine:init', () => {
    Alpine.store('theme', {
      dark: localStorage.theme === 'dark',
      toggle() {
        this.dark = !this.dark
        localStorage.theme = this.dark ? 'dark' : 'light'
        document.documentElement.classList.toggle('dark', this.dark)
      }
    })
  })
</script>

Then any block on the page can read or write $store.theme.dark. The dark-mode button calls $store.theme.toggle(). A header notification reads $store.theme.dark to render its icon. No prop-drilling, no event-bus, no third-party state library.

Casey: my snippets library has ten files in it. Every client engagement starts by copying three to five files out of it into the client's repo. The library has saved me at least 40 hours per quarter on net-new client projects. The library is also the single most valuable freelance asset I own that no client sees.

Alpine + client static-site stacks: Hugo, Eleventy, Astro, Jekyll, WordPress

Most freelance client sites in 2026 run on one of five stacks: WordPress, Hugo, Eleventy, Astro, or Jekyll. Alpine drops into all five with one line in the right partial. Here is each one, worked.

WordPress (custom child theme, the most common freelance stack)

Open the active child theme's functions.php. Add the Alpine CDN enqueue:

php
function client_enqueue_alpine() {
  wp_enqueue_script(
    'alpinejs',
    'https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js',
    array(),
    '3.14.1',
    array('strategy' => 'defer')
  );
}
add_action('wp_enqueue_scripts', 'client_enqueue_alpine');

The strategy => 'defer' option (WordPress 6.3 and later) adds the defer attribute. On older WordPress installations the third argument needs to be replaced with the older script-loader strategy filter, but most freelance projects in 2026 are on 6.4 or later. Once enqueued, Alpine attaches to any x-data block in the theme templates, in Gutenberg blocks, in classic-editor HTML, and in third-party page-builders that allow raw HTML (Elementor, Bricks, Oxygen, Beaver Builder).

The freelancer wedge on WordPress specifically: the client's marketing team can paste Alpine HTML blocks into a Gutenberg Custom HTML block on a new page WITHOUT touching functions.php. As long as the script is enqueued site-wide, every page picks up Alpine. That is the handoff advantage.

Hugo

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

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

Hugo's Goldmark markdown parser passes raw HTML through to rendered output by default. That means the client's blog posts in content/posts/*.md can contain <div x-data="{ open: false }">...</div> blocks. If the inherited Hugo theme has unsafe = false set in config.toml, you may need to flip it to unsafe = true:

toml
[markup.goldmark.renderer]
unsafe = true

That 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 embedded HTML in markdown.

Eleventy

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

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

Eleventy passes HTML through markdown by default via markdown-it. No config change needed for most freelance Eleventy projects. If the inherited project has a stricter markdown setup, set html: true in .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 processes <script> tags by default. For the Alpine CDN tag you want pass-through, which 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>

Without is:inline, Astro tries to bundle the script and the defer behavior breaks. With is:inline, Astro renders the tag as-is. Astro markdown files (.md and .mdx) pass raw HTML through to the renderer, so <div x-data> blocks work inside content collections.

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. The client's _posts/*.md files can contain Alpine blocks immediately.

Casey: I run Alpine on three WordPress clients, one Eleventy client, one Hugo client, and one Astro client in 2026. The WordPress integration via wp_enqueue_script is the smoothest because the client's marketing team can author Alpine blocks in Gutenberg without touching any PHP. The Astro is:inline trick is the only gotcha worth memorizing. Hugo, Eleventy, and Jekyll are paste-the-script-tag and you are done.

Client handoff: the HANDOFF.md template

Every Alpine engagement should ship with a HANDOFF.md file in the client's repo root. The freelancer-business wedge against React or Vue is precisely that Alpine produces a handoff document the client can actually read. Here is the template I commit on every engagement.

markdown
# Site interactivity (Alpine.js)

This site uses Alpine.js for interactive components. Alpine is a 15kb
JavaScript library that adds interactivity to plain HTML. The library
is loaded from a CDN at the top of every page. Everything in this
file is something a non-developer can edit safely.

## What Alpine is, in one paragraph

Alpine.js reads HTML attributes that start with `x-` and turns them
into interactive behavior. For example, `x-data="{ open: false }"`
declares a piece of state called `open` that starts as `false`.
`x-on:click="open = !open"` says "when this element is clicked, flip
the value of open." `x-show="open"` says "only show this element
when open is true." That is the entire model.

## Where Alpine is loaded

`<head>` of every page, via the CDN tag in `[theme/header.php OR
layouts/partials/head.html OR _includes/base.njk OR whatever]`.

## What you can edit safely

- The text inside an `<x-text>` block. Example:
  `<span x-text="faq.q"></span>` — change the source data, not the
  attribute.
- The list inside an `x-data="{ ... }"` block. Add a new FAQ to the
  `[{ q: '...', a: '...' }, { q: '...', a: '...' }]` array; the
  accordion picks it up automatically.
- The CSS classes on any Alpine element. Style the elements however
  you want.
- The content inside `<button>`, `<a>`, `<p>` tags. Plain HTML edits
  are safe.

## What NOT to edit without checking with me

- The `x-data="{ ... }"` attribute itself. Adding or removing keys
  changes the state shape.
- Any attribute starting with `x-on:`, `x-show:`, `x-bind:`. These
  are the wiring between state and behavior.
- The `<script defer src="...alpine...">` tag in the head. Removing
  it breaks every interactive component.
- The `[x-cloak] { display: none !important }` CSS rule. Removing
  it causes a brief flash of unprocessed content on page load.

## Where each interactive block lives

| Component | File | Lines |
|---|---|---|
| Newsletter signup | templates/footer.php | 22-50 |
| FAQ accordion | content/faq.md | 14-32 |
| Contact form | templates/contact.php | 8-46 |
| Dark-mode toggle | templates/header.php | 5-10 |
| Lazy YouTube embed | content/about.md | 80-95 |

## How to add a new FAQ

Open content/faq.md. Find the `x-data` block (around line 14).
Inside the `[ ... ]` array, add a new object:
`{ q: 'Your new question?', a: 'Your new answer.' }`. Save. The
accordion picks it up on next page load. No deploy step.

## Where to reach me

[Your email]. Average response within one business day. If
something is broken in production and you cannot wait, here is the
emergency rollback: revert the most recent commit to this repo. The
last known-good state is on the main branch.

Commit HANDOFF.md to the client's repo. Reference it in the closing email when you hand off. The document does two jobs at once: it lets the client's marketing person edit Alpine blocks themselves (which the client appreciates and you would not get from a React handoff), and it scopes the work so future "the site is broken" emails from the client are about things the document explicitly named as out-of-scope.

Pricing patterns: what to bill for Alpine work

The freelancer-business angle every developer-targeted tutorial skips. Here is the pricing framework I use across client engagements, refined over three years of Alpine projects.

Line itemScopeFixed-price rangeHourly equivalent
Alpine.js initial setupCDN tag + x-cloak CSS + smoke-test counter1-3 hours$90-$300
Per-component implementation (simple)FAQ accordion, dark-mode toggle, copy-link button, scroll bar30-60 minutes each$45-$150 each
Per-component implementation (with API)Newsletter signup, contact form (Formspree), Mailchimp integration1-2 hours each$90-$300 each
Multi-pattern bundle4-6 components shipped in one engagement4-8 hours$360-$1,200
Ongoing Alpine retainerBug fixes, micro-feature additions, FAQ content updates2-4 hours/month$180-$600/month
Migration from jQuery to AlpineReplace existing jQuery interactivity with Alpine on a client site6-16 hours$540-$2,400
Migration from a Webpack React build to static + AlpineReplace a single-page React landing with static HTML + Alpine12-24 hours$1,080-$3,600
HANDOFF.md documentationDocument every Alpine block, do-not-touch notes, edit-safe guide1-2 hours$90-$300

A few notes on quoting Alpine work to clients.

Bill Alpine setup as a line item. Do not bury it in a general "build the site" deliverable. The client sees the line, sees the price, and the price is small enough to feel like a no-brainer compared to "configure Webpack + React + Vite + a hydration model." Two hours and ninety dollars per hour. The client says yes.

Charge per component, not by total Alpine hours. A freelancer who quotes "Alpine work, 15 hours, $1,350" leaves money on the table. The same engagement broken into "Newsletter signup ($180), FAQ accordion ($90), Contact form ($270), Dark-mode toggle ($90), Image carousel ($135), Scroll-progress bar ($90), Copy-link button ($45), HANDOFF.md ($180), and Alpine setup ($180)" totals $1,260 and reads as nine concrete deliverables the client can map to a roadmap. The total can be slightly lower and feel like better value.

Quote the migration explicitly. If the client's existing site uses jQuery and the brief is "modernize the interactivity," scope the Alpine migration as its own line item with a per-component scope ("replace the existing jQuery slideToggle accordion with an Alpine x-show + x-transition accordion, $90"). The line-by-line scope protects you from the open-ended "modernize everything" trap.

Retainer for ongoing tweaks. After handoff, offer the client a $200-$500/month retainer for "ongoing Alpine maintenance: bug fixes, FAQ content updates, occasional new components." Most freelancer-Alpine relationships do not need a retainer (the Alpine code is stable and the client can edit content themselves). The retainer is offered as the safety net, and a meaningful percentage of clients say yes because Alpine handoffs leave them confident they can call you back without surprise. Recurring revenue at low effort is the freelancer-business endgame.

Casey: my best-converting freelancer line item in 2026 is "Alpine.js setup + 3 components + HANDOFF.md, fixed price, two-week delivery, $1,400." Clients understand it, IT signs off in 90 seconds, the work fits inside a four-day sprint, and the recurring referrals from those clients are the single biggest source of new business I have.

Alpine vs HTMX vs jQuery vs Vue for client work

Honest comparison. Jordan here on the objections, because every freelancer pitching Alpine to a client eventually hears "why not jQuery / why not React / why not HTMX." Here is the freelancer-framed answer for each.

Alpine vs HTMX. The two 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 event handlers inline in HTML." A freelance project that has a Rails or Django or Laravel backend, with server-driven search-as-you-type, paginated lists, or comment threads, is well-served by HTMX for the server-fragment swaps and Alpine for the purely-client interactivity (dropdowns, modals, theme toggles). Many of my client projects run both. The two libraries explicitly support each other. If the client's site is fully static (no backend at all, just HTML), HTMX has nothing to do; Alpine alone is the right call.

Alpine vs jQuery. Alpine is the modern replacement for jQuery in 2026 for client interactivity layers. The directive-on-attribute model is what jQuery's .click(), .toggle(), .fadeIn(), and .fadeOut() always wanted to be. Alpine is more declarative (read the HTML, see what happens) where jQuery is imperative (read the JS file, then mentally apply it to the HTML). Bundle size is comparable (jQuery 3.x slim is ~25kb gzipped; Alpine is ~15kb). Critical difference for client work: jQuery has fifteen years of "find a plugin on GitHub" temptation that compounds into a maintenance liability the next freelancer inherits. Alpine has one library, one runtime, one set of fifteen documented directives. The migration play from jQuery to Alpine is the single most-billable Alpine engagement in 2026.

Alpine vs Vue (or React, or Svelte). Vue, React, and Svelte are full SPA frameworks designed for client-rendered applications with their own routing, state, and component models. Alpine is a "sprinkles" framework designed to add interactivity to server-rendered or static HTML. For a freelancer's typical client project (a marketing site, a landing page, a CMS theme, an information-architecture-light product page), Alpine is the right pick because the surface is small and the build cost of a real framework is not justified. For a freelancer's atypical client project (a real single-page app with deep state, client-side routing, a complex form-builder, an admin dashboard with hundreds of components), Vue or React or Svelte are the right pick because the abstraction pays for itself at that complexity. The decision is project-by-project. Default to Alpine; reach for the heavier framework only when the brief specifically demands it.

Honest limits. Alpine is wrong for: client projects that involve drag-and-drop reordering across nested containers (Alpine's directive model gets ugly past a certain interactivity threshold), real-time collaboration features (Operational Transform or CRDTs need a real framework), or anything that needs server-rendered React on the client (Next.js, Remix territory). When the brief crosses that line, name it explicitly to the client and quote accordingly.

When Alpine.js is the WRONG pick for a freelance project

The section every developer-targeted tutorial skips. Pick a different tool in these three scenarios.

The client's product is a real single-page application. The client has hired you to build a web product where the URL changes without a full page reload, nested routes, dynamic route parameters, and client-side data fetching coordinated across pages. Alpine has no router. There is no Alpine equivalent of Next.js, Remix, Nuxt, or SvelteKit. Reach for a real framework. The freelancer-business advice is to scope the project as "SPA build" not "Alpine engagement" and quote accordingly.

The client needs heavy shared state across hundreds of components. Alpine has Alpine.store() for global state, and it works fine for theme state or auth state. But if the brief is "we need redux-style time-travel debugging, action middleware, normalized cache management across a hundred-component admin dashboard," Alpine's state model will feel thin. That is by design. Reach for Pinia (Vue), Zustand or Redux (React), or a Svelte store. Tell the client "this is a Vue or React project, not an Alpine project" and quote accordingly.

The client's user experience requires drag-and-drop reordering, infinite-scroll with virtualization, or real-time collaboration. These three patterns are the hardest things to build in client interactivity, and Alpine does not have first-party support for any of them. For drag-and-drop, reach for SortableJS plus Alpine wrappers, or move the feature to Vue with VueDraggable. For infinite-scroll virtualization, reach for a framework with built-in virtual-list components (TanStack Virtual, Solid Virtual). For real-time collaboration, the freelancer brief should include Yjs or Liveblocks plus a real framework.

The honest freelancer move when a project hits one of these scenarios: tell the client upfront, quote the heavier framework, and protect the client from a six-month rewrite the next freelancer would have to do anyway.

Frequently asked questions

Can I bill for Alpine setup as a separate line item?

Yes. Quote Alpine setup as a fixed-price line item in your engagement: typically 1-3 hours at your standard rate, with the deliverable being the CDN tag installed in the client's site head, the x-cloak CSS rule added to the main stylesheet, and a smoke-test counter component to confirm the install works. Most freelancers quote this between $90 and $300 depending on hourly rate. The line item is visible on the invoice, defensible in scope discussions, and small enough that clients approve without negotiation. From there, every additional Alpine component (FAQ accordion, contact form, dark-mode toggle) is its own per-component line item priced at 30-60 minutes for simple components and 1-2 hours for components with an API call. The line-by-line pricing makes the engagement read as concrete deliverables, not abstract "JavaScript work."

Alpine vs HTMX for freelance client work?

Complementary, not competitive. HTMX handles server-driven HTML fragment swaps (search-as-you-type backed by a real endpoint, paginated lists, comment threads, real-time-ish form responses). Alpine handles purely client-side state and event handlers (modal open / close, theme toggle, accordion expand / collapse, form-field validation). Many freelance projects ship both libraries on the same site: HTMX for the backend-coupled interactivity, Alpine for the client-only bits. If the client's site is fully static with no backend, you do not need HTMX; Alpine alone is the right call. If the client's site is a Rails / Django / Laravel app with a templating layer, HTMX plus Alpine is the modern equivalent of "jQuery plus a few server-rendered partials" that the freelance industry has been shipping for fifteen years.

Will my non-technical client be able to edit Alpine-enabled pages?

Yes, and this is the wedge that distinguishes Alpine from React for freelance client work. The Alpine attribute model means an FAQ accordion is <div x-data="{ open: false }"> followed by HTML that the client can pattern-match to "the thing I expand and collapse." The client's marketing person can copy the block into a new page in their CMS, change the question text inside the <span x-text>, change the answer text inside the <div x-text>, and ship without your involvement. Try that with a React <FAQ items={[...]} /> JSX block: the client cannot copy it into their CMS because it never renders without the build. Ship a HANDOFF.md file with every Alpine engagement (template inside this page) that documents which parts the client can edit safely (text content, CSS classes, the data array) and which parts they should leave alone (the x-data, x-on, x-bind attributes themselves).

Alpine vs jQuery in 2026?

Alpine is the modern replacement for jQuery in client interactivity layers. Bundle size is comparable (jQuery 3.x slim ~25kb; Alpine ~15kb). Alpine is more declarative (read the HTML attributes, see what happens) where jQuery is imperative (read the JS file, then mentally apply it). The compounding freelancer concern is plugin sprawl: jQuery has fifteen years of "find a plugin on GitHub" temptation that turns into a maintenance liability when the next freelancer inherits the site. Alpine has one library, one runtime, one set of fifteen documented directives, and a single maintainer (Caleb Porzio) shipping the framework actively in 2026. For a freelancer in 2026, the jQuery-to-Alpine migration is the single most-billable Alpine engagement on a client retainer. Quote it as a line item ("replace the existing jQuery slideToggle accordion with an Alpine x-show + x-transition accordion, $90"), do it per-component, and most clients are happy to pay because the modernization story sells itself to their IT.

Best Alpine + WordPress integration pattern?

Enqueue Alpine via wp_enqueue_script in your custom child theme's functions.php with the WordPress 6.3+ strategy => 'defer' option. That gives every page on the site the Alpine runtime without polluting individual template files. The freelancer wedge on WordPress specifically is that the client's marketing team can then paste Alpine HTML blocks into a Gutenberg Custom HTML block on a new page WITHOUT touching functions.php. As long as Alpine is enqueued site-wide, every page picks it up. For sites running Elementor, Bricks, Oxygen, Beaver Builder, or other page-builders that allow raw HTML widgets, the pattern works identically: the page-builder's "HTML" widget contains the Alpine block, the runtime is already loaded site-wide, and the interactivity works. Document this in the client's HANDOFF.md so the marketing team knows where they can author Alpine and where they cannot.

Is Alpine production-ready for paid client work?

Yes. Alpine 3.x is current in 2026, MIT-licensed per github.com/alpinejs/alpine, actively maintained by Caleb Porzio, and shipped inside Laravel Livewire (one of the most-deployed PHP application stacks in the world) and Tailwind UI's interactive component patterns (a paid component library used in production by hundreds of teams). Production-ready means actively maintained, deployed at scale, predictable behavior, and clean licensing for commercial use. Alpine meets all four. The honest limit is that Alpine is best for sprinkle-style interactivity, not for SPA-style products. A client marketing site, landing page, CMS theme, FAQ block, contact form, or info-architecture-light product page is well-served by Alpine. A client SPA with client-side routing, deep nested state, and a hundred-component admin dashboard wants Vue or React. Quote each engagement based on what the brief actually needs.

Where to go next

If you are also shipping a build tool to client repos, the persona-sibling guide is esbuild for freelancers which covers the multi-client config strategy and the HANDOFF.md pattern for build pipelines. For a different build-tool option that auto-detects most client project shapes, see Parcel for freelancers. If you are the indie content creator on your OWN site (not paid by clients), the LIVE persona-sibling is Alpine.js for content creators, which covers the same 10 patterns from the creator-on-own-site angle. For the directive reference and the broader Alpine landscape, see Alpine.js on Solomon Signal and the production gotchas at Alpine.js best practices. For component-library handoff documentation patterns, see Storybook for freelancers. Authoritative sources for Alpine itself are the official docs at alpinejs.dev and the source repository at github.com/alpinejs/alpine.

Casey: my advice to any freelancer reading this in 2026 is to make Alpine your default interactivity layer for any client engagement that does not specifically require a build pipeline. Save the ten patterns above to a folder on your laptop. Quote per-component. Ship a HANDOFF.md with every engagement. Watch your invoice close-rate go up and your "the site is broken" emails go down. Alpine is the freelancer-friendly framework the developer-targeted tutorials never wrote about. Take the trade.

Read the full Alpine.js for Freelancers: No-Build Client Interactivity That Hands Off Cleanly (2026) review

Alpine.js for Freelancers: No-Build Client Interactivity That Hands Off Cleanly (2026) Use Cases FAQ

Common questions about applying Alpine.js for Freelancers: No-Build Client Interactivity That Hands Off Cleanly (2026) to real workflows

Yes. Quote Alpine setup as a fixed-price line item: typically 1-3 hours at your standard rate ($90-$300), deliverable being the CDN tag installed, the x-cloak CSS rule added, and a smoke-test counter component to verify the install. From there, every additional Alpine component (FAQ accordion, contact form, dark-mode toggle) is its own per-component line item at 30-60 minutes for simple components and 1-2 hours for components with an API call. The line-by-line pricing makes the engagement read as concrete deliverables, not abstract JavaScript work.
Complementary, not competitive. HTMX handles server-driven HTML fragment swaps (search-as-you-type backed by a real endpoint, paginated lists, comment threads). Alpine handles purely client-side state and event handlers (modal open/close, theme toggle, accordion expand/collapse, form-field validation). Many freelance projects ship both on the same site: HTMX for backend-coupled interactivity, Alpine for client-only bits. If the client's site is fully static with no backend, you do not need HTMX; Alpine alone is the right call.
Yes, and this is the wedge that distinguishes Alpine from React for freelance client work. The Alpine attribute model means an FAQ accordion is plain HTML the client can pattern-match. The client's marketing person can copy the block into a new page in their CMS, change the question text inside x-text, change the answer text, and ship without your involvement. Ship a HANDOFF.md file with every Alpine engagement that documents which parts the client can edit safely (text content, CSS classes, the data array) and which parts they should leave alone (the x-data, x-on, x-bind attributes themselves).
Alpine is the modern replacement for jQuery in client interactivity layers. Bundle size is comparable (jQuery 3.x slim ~25kb; Alpine ~15kb). Alpine is more declarative (read the HTML attributes, see what happens) where jQuery is imperative (read the JS file, then mentally apply it). Critical difference: jQuery has fifteen years of 'find a plugin on GitHub' temptation that turns into a maintenance liability when the next freelancer inherits the site. Alpine has one library, one runtime, one set of fifteen documented directives. The jQuery-to-Alpine migration is the single most-billable Alpine engagement on a client retainer in 2026.
Enqueue Alpine via wp_enqueue_script in your custom child theme's functions.php with the WordPress 6.3+ strategy => 'defer' option. That gives every page on the site the Alpine runtime without polluting individual template files. The freelancer wedge on WordPress is that the client's marketing team can then paste Alpine HTML blocks into a Gutenberg Custom HTML block (or an Elementor / Bricks / Oxygen HTML widget) on a new page without touching functions.php. As long as Alpine is enqueued site-wide, every page picks it up. Document this in the client's HANDOFF.md.
Yes. Alpine 3.x is current in 2026, MIT-licensed per github.com/alpinejs/alpine, actively maintained by Caleb Porzio, and shipped inside Laravel Livewire (one of the most-deployed PHP stacks in the world) and Tailwind UI's interactive component patterns. Production-ready means actively maintained, deployed at scale, predictable behavior, and clean licensing for commercial use. Alpine meets all four. The honest limit is that Alpine is best for sprinkle-style interactivity, not for SPA-style products with client-side routing, deep nested state, or a hundred-component admin dashboard - those want Vue or React.