Home Blog How to Add Interactivity to Jekyll with Alpine.js
Tutorial

How to Add Interactivity to Jekyll with Alpine.js

Alpine.js is a lightweight JavaScript framework perfect for Jekyll sites. Learn how to add dropdowns, tabs, modals, accordions, and more without a build step.

How to Add Interactivity to Jekyll with Alpine.js

Jekyll produces static HTML — no JavaScript framework, no reactive state, just pages. That is a feature, not a bug. But sometimes you need a little interactivity: a mobile menu toggle, a tab component, an accordion, a modal. Writing vanilla JavaScript for each one is repetitive. React is massive overkill.

Alpine.js is the perfect middle ground. It is a 15kb library that adds reactive behaviour directly in your HTML — no build step, no component files, no bundler required.

What is Alpine.js?

Alpine.js lets you add JavaScript behaviour to HTML elements using special attributes: x-data, x-show, x-bind, x-on, and a handful of others. If you know Tailwind CSS, Alpine has the same philosophy applied to JavaScript — declare behaviour in your markup.

<div x-data="{ open: false }">
  <button @click="open = !open">Toggle</button>
  <div x-show="open">I am visible when open is true</div>
</div>

That is it. No component files, no state management, no compilation.

Adding Alpine.js to Jekyll

Option 1: CDN (simplest)

Add Alpine via CDN in your _layouts/default.html before the closing </body>:

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

The defer attribute ensures Alpine loads after the DOM is ready. This is all you need for most use cases.

Option 2: npm install

If you already have a Node.js build pipeline:

npm install alpinejs

Then import in your JS entry point:

import Alpine from "alpinejs";
window.Alpine = Alpine;
Alpine.start();

Practical examples for Jekyll sites

Mobile navigation menu

The most common use case on any Jekyll site:

<nav x-data="{ mobileOpen: false }">
  <div class="navbar__inner">
    <a href="/" class="navbar__logo">JekyllHub</a>

    <!-- Desktop links -->
    <ul class="navbar__links">
      <li><a href="/themes/">Browse</a></li>
      <li><a href="/blog/">Blog</a></li>
    </ul>

    <!-- Mobile toggle -->
    <button @click="mobileOpen = !mobileOpen" :aria-expanded="mobileOpen">
      <span x-show="!mobileOpen"></span>
      <span x-show="mobileOpen"></span>
    </button>
  </div>

  <!-- Mobile menu -->
  <div x-show="mobileOpen" x-transition @click.away="mobileOpen = false">
    <ul>
      <li><a href="/themes/">Browse</a></li>
      <li><a href="/blog/">Blog</a></li>
    </ul>
  </div>
</nav>

FAQ accordion

<div class="faq" x-data="{ active: null }">
  {% for item in site.data.faq %}
  <div class="faq-item">
    <button
      class="faq-question"
      @click="active = active === {{ forloop.index }} ? null : {{ forloop.index }}"
      :aria-expanded="active === {{ forloop.index }}"
    >
      {{ item.question }}
      <span x-text="active === {{ forloop.index }} ? '−' : '+'"></span>
    </button>
    <div
      class="faq-answer"
      x-show="active === {{ forloop.index }}"
      x-transition
    >
      {{ item.answer }}
    </div>
  </div>
  {% endfor %}
</div>

Tab component

<div x-data="{ tab: 'themes' }">
  <!-- Tab buttons -->
  <div class="tabs">
    <button
      @click="tab = 'themes'"
      :class="tab === 'themes' ? 'tab--active' : ''"
    >Themes</button>
    <button
      @click="tab = 'posts'"
      :class="tab === 'posts' ? 'tab--active' : ''"
    >Posts</button>
  </div>

  <!-- Tab panels -->
  <div x-show="tab === 'themes'">
    <!-- themes content -->
  </div>
  <div x-show="tab === 'posts'">
    <!-- posts content -->
  </div>
</div>
<div x-data="{ open: false, image: '' }">
  <!-- Trigger buttons on theme cards -->
  {% for theme in site.themes %}
  <button
    @click="open = true; image = '{{ theme.card_image | relative_url }}'"
  >
    <img src="{{ theme.card_image | relative_url }}" alt="{{ theme.title }}">
  </button>
  {% endfor %}

  <!-- Modal overlay -->
  <div
    x-show="open"
    x-transition
    @click="open = false"
    @keydown.escape.window="open = false"
    class="modal-overlay"
  >
    <div @click.stop class="modal-panel">
      <button @click="open = false" class="modal-close"></button>
      <img :src="image" class="modal-image">
    </div>
  </div>
</div>

Dark mode toggle

<button
  x-data
  @click="
    document.documentElement.classList.toggle('dark');
    localStorage.setItem('theme',
      document.documentElement.classList.contains('dark') ? 'dark' : 'light'
    )
  "
  aria-label="Toggle dark mode"
>
  🌙
</button>

On page load, restore the saved preference:

<script>
  if (localStorage.theme === 'dark' ||
      (!localStorage.theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
    document.documentElement.classList.add('dark');
  }
</script>

Search input filter

Filter a list of items client-side without a full search library:

<div x-data="{ query: '' }">
  <input
    x-model="query"
    type="search"
    placeholder="Filter themes..."
    class="search-input"
  >

  <div class="theme-grid">
    {% for theme in site.themes %}
    <div
      class="theme-card"
      x-show="'{{ theme.title | downcase }}'.includes(query.toLowerCase())"
    >
      <h3>{{ theme.title }}</h3>
    </div>
    {% endfor %}
  </div>
</div>

Alpine.js directives reference

Directive What it does
x-data Defines a reactive data scope
x-show Toggles element visibility
x-if Conditionally renders element (removes from DOM)
x-for Loops over an array
x-model Two-way data binding on inputs
x-text Sets element text content
x-html Sets inner HTML
x-bind or : Binds an attribute to a value
x-on or @ Attaches event listeners
x-transition Adds enter/leave CSS transitions
x-ref Gives an element a reference
x-cloak Hides element until Alpine initialises

Preventing flash of Alpine markup

Before Alpine initialises, x-show elements may flash visible. Prevent this with x-cloak:

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

<div x-data="{ open: false }" x-cloak>
  <div x-show="open">Hidden until Alpine loads</div>
</div>

Alpine.js vs vanilla JavaScript for Jekyll

For simple toggles and one-off interactions, vanilla JS is fine. Alpine becomes valuable when you have multiple interactive components that would otherwise require repetitive vanilla JS — menus, accordions, tabs, modals, filters. Alpine makes each one a few lines of HTML rather than a script block.

The 15kb cost is worth paying when you have three or more interactive components. If you only need one toggle on your entire site, a three-line vanilla JS function is lighter.

Alpine and Jekyll together give you the interactivity of a framework site with the speed and simplicity of a static one.

Share LinkedIn