A light-weight library for declarative DOM actions using data attributes
99% of static sites and server-rendered apps don't need heavy frameworks. Modern HTML and CSS are powerful, but sometimes you just need a touch of interactivity.
Attractive.js gives you declarative actions through data attributes. It is not a framework. It is just a humble set of actions. Write less. Ship more.
<button data-action="toggleAttribute#disabled=disabled" data-target="name">
Toggle disabled State
</button>
<button data-action="addAttribute#disabled=disabled" data-target="name">
Add disabled State
</button>
<button data-action="removeAttribute#disabled=disabled" data-target="name">
Remove disabled State
</button>
<input type="text" id="name" class="name" placeholder="Can you focus on me?">
<button
data-action="toggleAttribute#open"
data-target="details"
>
Toggle open
</button>
<details id="details">
<summary>
Want to see all the details?
</summary>
<p>These are some interesting details!</p>
</details>
<button
data-action="cycleAttribute#type=password,text"
data-target="password"
>
View password
</button>
<input type="password" id="password" value="my secret" class="name">
<button
data-action="addClass#bg-black"
data-target="door-1"
>
Paint it black
</button>
<p id="door-1">
Paint me black.
</p>
<button
data-action="removeClass#bg-black"
data-target="door-2"
>
Remove black paint
</button>
<p id="door-2" class="bg-black">
Paint me black.
</p>
<button
data-action="toggleClass#bg-black"
data-target="door-3"
>
Paint it back
</button>
<p id="door-3">
Paint me black.
</p>
<button
data-action="toggleClass#bg-black,text-white"
data-target="door-4"
>
Paint it black with white
</button>
<p id="door-4">
Paint me black.
</p>
<button
data-action="cycleClass#bg-black,bg-red-500"
data-target="door-5"
>
Paint it bled
</button>
<p id="door-5" class="bg-black">
Paint me black.
</p>
<button
data-action="copy"
data-target="share-link"
>
Copy
</button>
<input type="text" id="share-link" value="https://railsdesigner.com/articles/" readonly="" class="[[data-copy-success='true']]:border-green-500">
A data-copy-success="true" attribute/value is added on the target element which is used to add a green border after copied.
<button
data-action="copy"
data-target="access-code"
data-copy-delay="5000"
>
Copy
</button>
<p>
Your access code is:
<code id="access-code" class="[[data-copy-success='true']]:text-green-500">ghi-789-jkl-012</code>
</p>
Use data-copy-delay="n" (in ms) to set the delay before the data-copy-success={true,false} gets removed from the target element. This example has 5000 defined.
<button
id="failing-copy-button"
data-action="copy#could-not-be-copied"
class="[[data-copy-success='false']]:bg-red-100 [[data-copy-success='false']]:border-red-500"
>
Copy
</button>
When copying to clipboard fails, the attribute data-copy-success="false" is added to the target.
<a href="https://railsdesigner.com" data-action="confirm" data-confirm-message="Really visit my website?">
Visit my website
</a>
<input type="text" id="share-link" value="https://railsdesigner.com/articles/" readonly>
<button data-action="confirm copy" data-target="share-link">
Copy URL to my website
</button>
Confirm an action by setting confirm first
<button
data-action="addDataAttribute#tab=1"
data-target="tabs"
>
First tab
</button>
<button
data-action="addDataAttribute#tab=2"
data-target="#tabs"
>
Second tab
</button>
<button
data-action="addDataAttribute#tab=3"
data-target="#tabs"
>
Third tab
</button>
<ul id="tabs" data-tab="1">
<li class="hidden [[data-tab='1']_&]:block">
First tab content
</li>
<li class="hidden [[data-tab='2']_&]:block">
Second tab content
</li>
<li class="hidden [[data-tab='3']_&]:block">
Third tab content
</li>
</ul>
<button
data-action="toggleDataAttribute#status=busy"
data-target="bee-house"
>
Toggle busy status
</button>
<div id="bee-house" class="relative p-4 border">
Just some container that can be busy…
<p class="hidden absolute inset-0 items-center justify-center text-white bg-black/30 backdrop-blur-sm [[data-status='busy']_&]:flex">
It is busy! 🐝
</p>
</div>
<button
data-action="cycleDataAttribute#light=stop,caution,go"
data-target="traffic-light"
>
Cycle traffic light
</button>
<ul id="traffic-light" class="relative flex flex-col">
<li class="size-6 rounded-md bg-red-100 [[data-light='stop']_&]:bg-red-600"></li>
<li class="size-6 rounded-md bg-orange-100 [[data-light='caution']_&]:bg-orange-400"></li>
<li class="size-6 rounded-md bg-green-100 [[data-light='go']_&]:bg-green-600"></li>
</ul>
<button data-action="dialog#open" data-target="message">
View dialog
</button>
<dialog id="message" class="m-auto p-2 border border-gray-200 rounded-md">
<h2 class="text-lg font-bold">Feedback</h2>
<p>Let me know what you think of Attractive.js 🤙</p>
<form method="dialog" class="mt-2">
<button type="button" data-action="dialog#close" data-target="message">Cancel</button>
<button type="submit">Submit</button>
</form>
</dialog>
<button data-action="dialog#openModal" data-target="with-backdrop">
View dialog with backdrop
</button>
<dialog id="with-backdrop" closedby="any" class="p-2 m-auto">
<h2 class="text-lg font-bold">Modal Heading</h2>
<p>I got a message for you! Press Escape or click outside to close</p>
</dialog>
Use closedby="any" to allow close by clicking outside.
<button data-action="element#remove">
Remove this button (click me)
</button>
<button data-action="element#remove" data-target="removable-item">
Remove target element
</button>
<p id="removable-item" class="p-4 bg-gray-50">
This element will be removed when the button is clicked.
</p>
<button
data-action="element#remove"
data-target="delayed-item"
data-remove-delay="2000"
>
Remove after 2 seconds
</button>
<p id="delayed-item" class="p-4 bg-gray-50">
This element will be removed 2 seconds after clicking the button.
</p>
Use data-remove-delay to specify after how many ms the element should be removed.
<button
data-action="element#add"
data-target="list-1"
data-add-source="#item-template"
>
Add item
</button>
<ul
id="list-1"
class="grid gap-2"
>
<li class="p-2 bg-gray-50">Existing item</li>
</ul>
<template id="item-template">
<li class="p-2 bg-gray-50">New item</li>
</template>
The added content is pulled from data-add-source. It can be a typical HTML element or a template. Use data-add-at='*' to specify where the added items need inserted. Valid options: beforeend (default), beforebegin, beforeend, afterend.
<button
data-action="form#reset"
data-target="reset-form"
>
Reset form
</button>
<form id="reset-form" method="get" action="/#actions" class="p-2 border border-gray-100 rounded-sm">
<label for="email">Email</label>
<input type="email" placeholder="Enter something to see it reset again">
<label for="name">Name</label>
<input type="text" value="Rails Designer">
</form>
Allows to reset a form even when the button is outside of the form. Only fields without a value are reset.
<form id="preferences" method="get" action="/#actions" class="p-2 border border-gray-100 rounded-sm">
<label for="framework">Framework (submits form on select)</label>
<select
id="framework"
name="framework"
data-action="form#submit"
data-target="preferences"
>
<option value="">Choose your JS library</option>
<option value="attractive">attractive.js</option>
<option value="turbo">turbo</option>
<option value="stimulus">stimulus</option>
</select>
</form>
This add ?framework=* to the url since the form has method=get.
<form id="more-preferences" method="get" action="/#actions" class="p-2 border border-gray-100 rounded-sm">
<label for="framework">Framework (submits form on select)</label>
<select
id="framework"
name="framework"
data-action="form#submit"
data-target="more-preferences"
data-submit-delay="2000"
>
<option value="">Choose your JS library</option>
<option value="attractive">attractive.js</option>
<option value="turbo">turbo</option>
<option value="stimulus">stimulus</option>
</select>
</form>
Use data-submit-delay="n" (where n is in ms) to add a debounce/delay before submit. This example uses 2000ms.
<button
data-action="reload"
data-target="turbo-frame"
>
Reload
</button>
<turbo-frame></turbo-frame>
<button data-action="reload">
Reload
</button>
With no data-target the page is refreshed. Note: reload is aliased to refresh.
<button
type="button"
data-action="get#/"
data-target="content"
>
Load this homepage below
</button>
<div id="content" class="max-h-96 overflow-scroll">
<span class="hidden [[data-request-busy='true']_&]:block">Loading…</span>
</div>
When loading data-request-busy="true" is added, and upon success data-request-success="true" is added, to the target.
<select name="theme" id="theme" data-action="patch#/preferences">
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
<ul class="w-full h-32 overflow-y-auto border">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
<li>Item 6</li>
<li>Item 7</li>
<li>Item 8</li>
<li>Item 9</li>
<li>Item 10</li>
<li>Item 11</li>
<li>Item 12</li>
<li>Item 13</li>
<li>Item 14</li>
<li data-action="scrollTo#smooth" class="bg-amber-100">Item 15 (target)</li>
<li>Item 16</li>
<li>Item 17</li>
<li>Item 18</li>
<li>Item 19</li>
<li>Item 20</li>
</ul>
Other valid options are instant and auto (which will inherit the value defined in CSS)