data-* Attributes: The Contract Between Your HTML, CSS and JS
The humble data attribute is the closest thing frontend has to a universal interface: the server renders it, JavaScript reads and writes it, CSS styles against it - and none of the three needs to know the others exist.
The JS side: dataset
<button data-product-id="42" data-action="add-to-cart" data-stock="3">Add</button>
const btn = e.target.closest('[data-action="add-to-cart"]');
btn.dataset.productId; // "42" - note the camelCase conversion
btn.dataset.stock = '2'; // writes data-stock="2" back to the DOM
Two things everyone trips on once: kebab-case attributes become camelCase properties (data-product-id → dataset.productId), and everything is a string. dataset.stock is "3", so Number() it before math - "3" - 1 works by accident, "3" + 1 gives "31" by betrayal.
Delegation plus data-action (as above) is my favorite jQuery-free event architecture: one listener on the container, behavior declared in markup, works for AJAX-injected elements automatically - the delegation pattern with the config living in HTML.
The CSS side: selectors and state
[data-stock="0"] { opacity: 0.5; pointer-events: none; }
[data-theme="dark"] { --bg: #0f172a; --text: #e2e8f0; }
[data-loading="true"] .label { visibility: hidden; }
Toggling el.dataset.loading = 'true' from JS and letting CSS own the visual consequences beats classList juggling when the state has multiple values - data-state="idle|loading|error|done" with four attribute selectors reads like a state machine, because it is one.
The party trick: attr() puts them on screen
.tooltip-host::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%; left: 50%;
translate: -50% -6px;
padding: 4px 10px;
background: #1e293b; color: #fff;
border-radius: 6px; font-size: 12px;
opacity: 0;
transition: opacity 0.15s;
pointer-events: none;
}
.tooltip-host:hover::after,
.tooltip-host:focus-visible::after { opacity: 1; }
<button class="tooltip-host" data-tooltip="Download as CSV">⬇</button>
A complete tooltip system with zero JavaScript - change the attribute, the tooltip changes. (Note: in content, attr() returns strings only; the typed attr() for other properties is still rolling out, so keep this trick to content.) Also note real tooltips for critical info deserve aria-label too; this pattern shines for supplementary hints.
The philosophy underneath: data attributes decouple. The PHP template declares facts; JS adds behavior to facts it finds; CSS styles facts it can see. Any of the three can change independently - which is exactly what "maintainable" means in practice.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.