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.
<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
.
<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)