Attractive.js 1.0.0: interactive HTML without one line of JavaScript

I wrote a JavaScript library. Again (or rather I rewrote the JavaScript library I wrote some time ago).

I had the idea for Attractive for quite some time, based on the following thought: why do I reach for a framework to toggle a class, add an attribute or copy a bit of text?

So I made Attractive.js: a humble set of declarative HTML actions. You put the behavior on the element itself. No virtual DOM, no state management, no component lifecycle to learn or more of things like that. Reading the markup tells you what the page does. In that regard it is much like Stimulus, but with less ceremony/setup (and Attractive gives you actions out of the gate).

This is an overview of what 1.0.0 looks like (currently available as pre-release).

In a nutshell:

  • complete new syntax
  • extensibility (create your own custom actions)
  • addons like keyboard, reactive, validate and attract (to make Attractive.js a worthy competitor for Hotwire!)
  • A base class to write custom elements, named Attractive Element

This week I cover the basics of Attractive.js, next week I go over how to write custom actions and the week following it all comes together in a real Rails app where I show how it can replace Hotwire. But first…

What’s new in 1.0.0

The biggest change is the syntax. Actions used to live in data-action and data-target. Now they’re just @action and @target.

<button @action="toggleClass#active" @target="panel">Toggle</button>

<div id="panel">Content</div>

That’s the whole interaction: an attribute that says what happens and one that says where.

For existing users, it is just a rename:

-<button data-action="mouseenter->addClass#hovered">
+<button @mouseenter="addClass#hovered">

The event-type attributes read like sentences, they’re easier to find in a grep and the old -> bulky syntax is gone. The @ shorthand means the same as @action. And simple setups now work from a single script tag with the activate attribute, no manual Attractive.activate() call:

<script src="//unpkg.com/attractivejs" type="module" activate></script>

The actions

Everyday interactions are covered out of the box. Class and attribute actions (toggleClass, addClass, removeClass, cycleClass, the same for attributes and data attributes), style actions, copy to clipboard, confirm, dialogs, focus, form submit and reset, scroll to and more.

There’s also a js: prefix for the occasional one-liner:

<button @action="js:this.textContent=1">0</button>

Handy when a custom action is a bit too much. For anything bigger, you can write your own action (which is next week’s post).

Triggers and gates

Beyond actions, you can control when they run with triggers and gates, appended to the value with a colon.

Triggers run an action without user input. :mounted runs immediately when the element appears. :whenVisible fires when it scrolls into view:

<img
  @action="addClass#loaded:whenVisible"
  data-src="/image.jpg"
  loading="lazy"
/>

Gates guard an action. :once runs it a single time. :preventDefault calls event.preventDefault(). :whenOutside only fires when the click target is outside the element, which is how the dropdown example from below closes itself:

<button @action="toggleClass#open:once">Click once</button>

<div @action="removeClass#open:whenOutside" @target="menu">…</div>

You can register your own triggers and gates too, with addTriggers and addGates.

Minimal and Powerful CMS for Static Sites

Work together with your team on all your content, from blog, to docs to changelog.

Start trial

What you can build with zero JavaScript

To show what this means in practice, here are a few components you can write without a line of your own JavaScript.

A copy button is one attribute pointing at the target to copy an API key , code example and so on:

<button @action="copy" @target="card-example">Copy</button>

<code id="card-example">…</code>

A dialog (with a newsletter sign up) opens at a certain point within your article:

<div @action="openModal:whenInView" @target="newsletter"></div>

<dialog id="newletter">…</dialog>

Auto-save form data with a debounce:

<form action="POST" id="post">
  <textarea name="body" @action="submit" @target="post" data-debounce="400"></textarea>
</form>

Tabs and accordions are toggleAttribute against the right target. Toasts remove themselves with a :mounted trigger and a data-delay=8000. Even a carousel is just cycleClass on a custom property.

The default event per element type means a button just works with @action alone.

You can see all actions in action in the docs.

How small is it?

The full build is about ~7 KB gzipped (no external dependencies). If you’re feeling fancy, the core engine is less than 5 KB and you can import only the actions you use. That only applies to a bundler setup, though. With importmap, you pin one minified file and you’re done. Either way, it’s a fraction of what you’d drag in for a framework.

Where I’d use this

Attractive.js works great on static sites for those small bits of interactivity, but above examples also show how it works great for server-rendered apps or to replace those bulky toggle_class_controller.js 😅

The core has been solid for a while. Try it with the script tag from the quickstart and if you want to make me happy (besides giving Attractive a try), star the repo. 🌟

Next week: writing your own actions. After that: putting all of it to work in a Rails app.

Product-minded Rails notes

Once a month: straightforward notes on improving UX in Rails—what to simplify, what to measure, and UI/frontend changes that move real usage.

Over to you…

What did you like about this article? Learned something knew? Found something is missing or even broken? 🫣 Let me (and others) know!

Comments are powered by Chirp Form

Want to read me more?