petite-vue is a new cut of the Vue project specifically built with progressive enhancement in mind. At 5kb, petite-vue is a lightweight Alpine (or jQuery) alternative that can be “sprinkled” over your project requiring no extra bundling steps or build processes. Add a <script> tag, set a v-scope, and you’re off to the races. This is up my alley.

Since it came out, I’ve been looking for a quick project to road test petite-vue with, but I’m a bit buried under client work and starting a new company to dream up some new toy projects. This weekend I was updating my bookshelf and realized petite-vue would be an ideal candidate for a feature I’ve wanted for awhile: Filter By Tags.

Sprinkling in petite-vue

Some background: My site is a statically generated Jekyll site with 790 bytes of first-party JavaScript. All the data that powers my bookshelf lives in a giant YAML file in _data/books.yml. This system has become less sustainable now that there are 200+ books on the bookshelf but that’s a project for another day. If you want a gist of how I built my bookshelf, here’s the gist of how I built my bookshelf.

For years now, I’ve dreamed about rewriting this whole thing in Vue so that I could add some interactive/reactive category filtering, but introducing a JS build chain into my Jekyll project always stopped me dead in my tracks. I don’t need the whole shebang, I need a place to store some state and some directives to make content show/hide based on that state. petite-vue is a perfect fit for this niche!

Step 1: Initialize petite-vue

As with most little tutorials, you start by adding the thing to the thing. I added this to the bottom of my bookshelf.html

<script type="module">
  import { createApp } from ''

Step 2: Add v-scope and my page state

Next we need to add a v-scope directive to the place we want Vue to mount. v-scope also does double duty by being the place where you set your components default state. Handy.

<div v-scope="{ filter: undefined }" class="bookshelf">

Step 3: Add v-show to each book

Next I wired up the show/hide logic on the book itself. If filter isn’t set, show the book, otherwise check if the book’s tags contain the current filter.

<div class="book" v-show='!filter || {{ book.tags | jsonify }}.includes(filter)'>

This looks a little bit weird because both Vue and Liquid use {{ curly_braces }} . In this line, {{ book.tags | jsonify }} is Liquid code that spits out a JavaScript array like ['graphic novel', 'biography']. If you were to view source, that line looks more like normal Vue code:

<div class="book" v-show='!filter || ['graphic novel', 'biography'].includes(filter)'>

Step 4: Create the filtering mechanism

Next is wiring up the code to set the filter. I originally had a bunch of <button> elements that set the filter, but I would have had to set aria-pressed and a bunch of other junk, I felt like a radio group semantically matched what I was trying to express. If this was a poor accessibility choice or this could be better, let me know, but I personally enjoy the free keyboard interactivity.

{% raw %}
  <fieldset class="filtering" @mounted="$el.hidden = false;" hidden>
    <div class="overflow">
        <input name="filter" id="all" type="radio" v-model="filter" :value="undefined" checked><label for="all">All</label>
      <div v-for="(tag, index) in [ 'Biography', 'Business', 'Graphic Novel', 'Management', 'Politics', 'Pop-sci', 'Sci-fi', 'Social Justice', 'Technology' ]" 
        <input name="filter" 
        <label :for="encodeURI(tag)" v-text="tag"></label>
{% endraw %}

In this situation, we don’t want Liquid to run, so we use the {% raw %} block to tell Liquid to ignore this block of code. From there it’s a standard Vue template. Selecting one of the radio buttons updates the filter state via the v-model directive. That update then cascades to show/hide each book based on that books v-show boolean statement. The end result is something that’s snappy and reactive and didn’t take much time at all.

One little piece of magic I’m proud of was this little hidden attribute flipper:

<div @mounted="$el.hidden = false;" hidden>

The UI elements inside will only show up if Vue gets mounted. That’s progressive enhancement, baby! I’ll probably use this pattern often in JavaScript-required scenarios on an otherwise static site. I’d even go so far to say as it’s an upgrade from the old .js-enabled pattern because it happens at the component level. And this is what I like about petite-vue, how it moves the needle a bit back closer to a JavaScript is optional mindset or at least the JavaScript can be additive and not all encompassing.

My complete petite-vue review

petite-vue is great for adding a little interactivity or reactivity to legacy projects or projects that you intentionally want to keep somewhat simple in scope. You don’t need to bring “Vue and friends” to the party to make a web form turn into a smiley face (or whatever the kids are doing these days). Vue’s design has always supported grafting into older projects with its Vue.component API, but petite-vue gives a bit more of an “automagic” feel by enhancing your otherwise normal HTML with minimal effort and minimal lines of code.

The feel of petite-vue is akin to Alpine.js, but because I’m comfortable in Vue I enjoyed this pathway more than my previous forays into Alpine. And I certainly enjoyed petite-vue more than making a querySelector soup of data-* attributes. “Sprinkling” in Vue directives to (optionally) enhance content makes this feel like a good way to manage interactive DOM in a predictable manner.

No bumps on my first voyage and was able to knock my idea out in an hour or so from when my first whim struck. That’s a positive signal for me that petite-vue is something I’ll be using more in the future. There’s also good potential that I can prototype projects in petite-vue and upgrade to grown-up Vue or Nuxt when needed. That’s a big improvement in my prototyping process becuase now the prototype will be a smidge closer to the target framework of the final product.