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#https://railsdesigner.com/"
>
Copy
</button>
<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-duration="5000"
>
Copy
</button>
<p id="access-code" class="[[data-copy-success='true']]:text-green-500">Your access code is: <code id="access-code">ghi-789-jkl-012</code></p>
Use data-copy-duration="n" (in ms) to set the duration for the data-copy-success={true,false} to be added to the target element. This example has 5000 defined.
<button
data-action="copy#`Spaceman, I always wanted you to go 🎶`"
>
Copy
</button>
Use backticks to use spaces in your copyable value.
<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="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.
<div class="w-full h-48 border overflow-y-auto">
<p
data-action="intersect-once#opacity-100"
class="mt-48 border-y p-4 opacity-0 transition-opacity duration-800 starting:opacity-0"
>
👻 “I was invisible, but now everyone can see me! Why did you scroll?”
</p>
</div>
Scroll the element to see opacity-100 added once it gets into the viewport. Transition via the starting class, e.g. starting:opacity-0.
<div class="relative w-full h-32 border overflow-y-auto bg-gradient-to-l from-cyan-400 to-sky-400">
<div id="sticky" class="sticky top-0 left-0 w-full p-2 text-center border-b">
Sticky element
</div>
<p
data-action="intersect-toggle#bg-white/80,backdrop-blur-[3px]"
data-target="#sticky"
class="my-32 border-y p-4"
>
Just some text to view behind the sticky element.
</p>
</div>
Toggle the classes bg-white/80 and backdrop-blur-[3px] when the element is within the viewport.
<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)