Interactivity with Alpine.js

On this page

Interactivity with Alpine.js

Every page built by BxSites already loads Alpine.js

  • it's what powers the built-in dark-mode toggle and language dropdown in every built-in theme. That same Alpine instance is available to your own page content too, for free: no bxsites.yaml setting to flip, no extraJs entry to add, no extra <script> tag to write in your markdown.

Since raw block-level HTML passes through untouched in your markdown, you can drop Alpine's x-data/x-show/@click/etc. attributes straight onto any HTML block and it just works.

Before reaching for Alpine

Most "interactive" needs already have a purpose-built directive block that doesn't require writing any JS yourself - reach for these first:

  • A collapsible section → Expandable or a collapsible admonition
  • Grouped alternative content behind clickable tabs → Content Tabs
  • A numbered walkthrough → Stepper
  • A styled call-to-action link → Buttons (the copy-to-clipboard button below is a different case - it has no href at all, just client-side behavior - which is exactly what Alpine is for)

Alpine is for the interactive content those don't cover - anything with its own client-side state.

A copy-to-clipboard button

::: button only ever renders a real link (or an inert placeholder) - like GitBook's own button, it has no notion of running arbitrary JS on click. For a button that does something instead of navigating somewhere, drop its bxsites-button/bxsites-button--* classes onto a plain HTML <button> instead - same look, styled in every built-in theme, just wired up with Alpine rather than an href. A common one: a button next to an install command that copies it and confirms the copy:

<div x-data="{ copied: false }">
  <button type="button" class="bxsites-button bxsites-button--secondary bxsites-button--small"
    @click="navigator.clipboard.writeText( 'box install bx-sites' ); copied = true; setTimeout( () => copied = false, 1500 )">
    <span x-show="!copied">Copy install command</span>
    <span x-show="copied" x-cloak>Copied!</span>
  </button>
</div>

A live filter

Filtering a list client-side, no server round-trip:

<div x-data="{ query: '' }">
  <input type="text" x-model="query" placeholder="Filter providers...">
  <ul>
    <li x-show="'local'.includes( query.toLowerCase() )">local (static index, no server)</li>
    <li x-show="'algolia'.includes( query.toLowerCase() )">algolia (hosted DocSearch)</li>
    <li x-show="'pagefind'.includes( query.toLowerCase() )">pagefind (indexed at build time)</li>
  </ul>
</div>

x-model binds the input's value to Alpine state; each <li>'s x-show re-evaluates on every keystroke.

A sortable, filterable table

A native pipe table is static once built - for one that a reader can actually sort and filter client-side (the closest thing here to GitBook's table search/sort), have Alpine own the rows instead: put the data in x-data and render it with x-for, rather than writing | Feature | Status | pipe syntax:

<div x-data="{
  query: '',
  sortKey: 'name',
  sortAsc: true,
  rows: [
    { name: 'Bootstrap', type: 'Components', stars: 4 },
    { name: 'GitBook', type: 'SaaS', stars: 5 },
    { name: 'Docusaurus', type: 'React', stars: 4 },
    { name: 'VuePress', type: 'Vue', stars: 3 }
  ],
  sortBy(key) {
    this.sortAsc = this.sortKey === key ? !this.sortAsc : true
    this.sortKey = key
  },
  get sorted() {
    return [...this.rows]
      .filter(r => r.name.toLowerCase().includes(this.query.toLowerCase()))
      .sort((a, b) => {
        const dir = this.sortAsc ? 1 : -1
        return a[this.sortKey] > b[this.sortKey] ? dir : a[this.sortKey] < b[this.sortKey] ? -dir : 0
      })
  }
}">
  <input type="text" x-model="query" placeholder="Filter by name...">
  <table class="table">
    <thead>
      <tr>
        <th @click="sortBy('name')" style="cursor:pointer">Name</th>
        <th @click="sortBy('type')" style="cursor:pointer">Type</th>
        <th @click="sortBy('stars')" style="cursor:pointer">Stars</th>
      </tr>
    </thead>
    <tbody>
      <template x-for="row in sorted" :key="row.name">
        <tr>
          <td x-text="row.name"></td>
          <td x-text="row.type"></td>
          <td x-text="row.stars"></td>
        </tr>
      </template>
    </tbody>
  </table>
</div>

Which renders as (type in the box, click a column heading):

Name Type Stars

rows is a plain JS array baked right into the page - fine for the kind of small reference table docs actually have. sorted is an Alpine getter, so it re-filters and re-sorts on every keystroke/click with no extra wiring; sortBy() toggles direction on a second click of the same column. The <table> here is a real <table> tag written by hand (there's no pipe-table syntax to hand rows to Alpine directly), so it still gets wrapped in .bxsites-table-wrap and the responsive scroll/sticky header treatment automatically, same as any table bx-markdown itself renders.

x-data fundamentals, if you're new to Alpine

x-data declares a scope's own reactive state as a plain JS object; anything inside that element can read/write it, and x-show/x-text/ x-model/@click (shorthand for x-on:click) all react to it changing:

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

See Alpine's own documentation for the full directive list (x-if, x-for, x-transition, and more).

Things to know

  • It's core, not optional. The theme chrome (dark mode, language switcher) depends on Alpine, so it can't be turned off in bxsites.yaml the way mermaid/math can.
  • Version. Currently alpinejs@3.14.1, vendored with this module and served from site/assets/vendor/alpine/ - no CDN involved. Check a theme's own layout.bxm for the exact <script> tag if you need to know precisely what's loaded.
  • Strict CSP. Alpine's default build evaluates the JS expressions inside x-data/@click etc. directly, which needs unsafe-eval under a strict Content-Security-Policy. If your deployment can't allow that, don't rely on Alpine in your page content.
  • Keep it light. A docs page should stay fast and simple - small, self-contained widgets (a copy button, a filter, a toggle) are a good fit; a full client-side app isn't what this is for.
Edit this page Download Markdown Last updated Aug 28, 2026, 3:16:38 AM