Skip to content

Commit ff100fb

Browse files
authored
feat: add data-act-attribute to set an arbitrary attribute on active theme controls (#56)
1 parent bf97764 commit ff100fb

11 files changed

Lines changed: 84 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
44

5+
## [Unreleased]
6+
7+
### Features
8+
9+
* added `data-act-attribute="name[:value]"` to set an arbitrary HTML attribute (e.g. `aria-pressed`, `aria-current`) on a button while its theme is active, as a more flexible alternative to `data-act-class` ([#41](https://github.com/saadeghi/theme-change/issues/41))
10+
* `data-act-attribute` also works with legacy `data-toggle-theme` buttons
11+
12+
### Bug Fixes
13+
14+
* legacy `data-toggle-theme` now recomputes each button's active state on click instead of blindly toggling `data-act-class`, fixing act-class desync with multi-theme toggle lists
15+
516
## [3.0.1] (2026-06-07)
617

718
### Features

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,26 @@ Notes:
144144
<button data-set-theme="pink" data-act-class="ACTIVE"></button>
145145
```
146146

147+
### `data-act-attribute`
148+
149+
Like `data-act-class`, but sets an arbitrary HTML attribute (instead of a class) on a button while its theme is active, and removes it when inactive. Useful for accessibility state such as `aria-pressed` or `aria-current`.
150+
151+
```html
152+
<!-- name:value — active: aria-pressed="true", inactive: attribute removed -->
153+
<button data-set-theme="dark" data-act-attribute="aria-pressed:true"></button>
154+
155+
<!-- bare name (no colon) — boolean presence: active: aria-current="", inactive: removed -->
156+
<button data-set-theme="pink" data-act-attribute="aria-current"></button>
157+
```
158+
159+
Notes:
160+
161+
- Syntax is `name[:value]`. Only the **first** colon splits name and value, so the value may itself contain colons (e.g. `data-act-attribute="title:Active: dark"`).
162+
- When inactive the attribute is fully removed (not set to a falsy value).
163+
- Button-only, same as `data-act-class` — ignored on `<select>` and checkbox controls.
164+
- Can be combined with `data-act-class` on the same element.
165+
- Also works with the legacy `data-toggle-theme` buttons.
166+
147167
### `data-key`
148168

149169
```html

btn.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
var T="theme";function i(t){return t||T}function c(t){return t||null}function l(t){return(t||"").split(",").map(e=>e.trim()).filter(Boolean)}function A(t,e){if(!t.length)return null;let n=t.indexOf(e||"");return n===-1?t[0]:t[(n+1)%t.length]}function d(t,e){let n=l(t);return n.length>1?A(n,e):c(t)}function h(t,e){let n=l(t);return e?n.length>1?n.includes(e):t===e:t===""}var D=i("");function p(t){return i(t&&t.getAttribute("data-key"))}function v(t,e){t?(document.documentElement.setAttribute("data-theme",t),localStorage.setItem(e,t)):(document.documentElement.removeAttribute("data-theme"),localStorage.removeItem(e))}function L(t){return localStorage.getItem(t)||null}function b(t,e){return p(t)===e}function x(t){return[...document.querySelectorAll("[data-set-theme]")].filter(e=>b(e,t))}var a=new WeakSet;function s(t,e,n,o){e.has(t)||(e.add(t),t.addEventListener(n,o))}function y(t,e){let n=t.getAttribute("data-act-class");n&&(e?t.classList.add(n):t.classList.remove(n))}function E(t,e){x(e).forEach(o=>{if(o instanceof HTMLSelectElement){let f=t||"";o.querySelector(`[value="${f}"]`)&&(o.value=f);return}if(o instanceof HTMLInputElement&&o.type==="checkbox"){o.checked=!!t&&o.value===t;return}let r=o.getAttribute("data-set-theme")||"",u=h(r,t);y(o,u)})}function m(t,e){v(t,e),E(t,e)}function F(t){let e=x(t);if(!e.length)return;let n=L(t);n&&document.documentElement.setAttribute("data-theme",n),E(n,t),e.forEach(o=>{if(o instanceof HTMLSelectElement){s(o,a,"change",function(){m(c(this.value),t)});return}if(o instanceof HTMLInputElement&&o.type==="checkbox"){s(o,a,"change",function(){let r=c(this.value);m(this.checked?r:null,t)});return}s(o,a,"click",function(){let r=this.getAttribute("data-set-theme")||"",u=document.documentElement.getAttribute("data-theme");m(d(r,u),t)})})}function S(){let t=[...document.querySelectorAll("[data-set-theme]")];if(!t.length)return;[...new Set(t.map(n=>p(n)))].forEach(n=>F(n))}function g(){S()}function q(t=!0){t?document.addEventListener("DOMContentLoaded",g):g()}export{g as themeBtn,q as themeChange};
1+
var S="theme";function u(t){return t||S}function i(t){return t||null}function l(t){return(t||"").split(",").map(e=>e.trim()).filter(Boolean)}function T(t,e){if(!t.length)return null;let n=t.indexOf(e||"");return n===-1?t[0]:t[(n+1)%t.length]}function d(t,e){let n=l(t);return n.length>1?T(n,e):i(t)}function h(t,e){let n=l(t);return e?n.length>1?n.includes(e):t===e:t===""}function p(t){let e=(t||"").trim();if(!e)return null;let n=e.indexOf(":"),r=(n===-1?e:e.slice(0,n)).trim();return r?{name:r,value:n===-1?"":e.slice(n+1)}:null}var H=u("");function A(t){return u(t&&t.getAttribute("data-key"))}function b(t,e){t?(document.documentElement.setAttribute("data-theme",t),localStorage.setItem(e,t)):(document.documentElement.removeAttribute("data-theme"),localStorage.removeItem(e))}function L(t){return localStorage.getItem(t)||null}function y(t,e){return A(t)===e}function x(t){return[...document.querySelectorAll("[data-set-theme]")].filter(e=>y(e,t))}var a=new WeakSet;function s(t,e,n,r){e.has(t)||(e.add(t),t.addEventListener(n,r))}function F(t,e){let n=t.getAttribute("data-act-class");n&&(e?t.classList.add(n):t.classList.remove(n))}function I(t,e){let n=p(t.getAttribute("data-act-attribute"));n&&(e?t.setAttribute(n.name,n.value):t.removeAttribute(n.name))}function v(t,e){x(e).forEach(r=>{if(r instanceof HTMLSelectElement){let f=t||"";r.querySelector(`[value="${f}"]`)&&(r.value=f);return}if(r instanceof HTMLInputElement&&r.type==="checkbox"){r.checked=!!t&&r.value===t;return}let o=r.getAttribute("data-set-theme")||"",c=h(o,t);F(r,c),I(r,c)})}function m(t,e){b(t,e),v(t,e)}function M(t){let e=x(t);if(!e.length)return;let n=L(t);n&&document.documentElement.setAttribute("data-theme",n),v(n,t),e.forEach(r=>{if(r instanceof HTMLSelectElement){s(r,a,"change",function(){m(i(this.value),t)});return}if(r instanceof HTMLInputElement&&r.type==="checkbox"){s(r,a,"change",function(){let o=i(this.value);m(this.checked?o:null,t)});return}s(r,a,"click",function(){let o=this.getAttribute("data-set-theme")||"",c=document.documentElement.getAttribute("data-theme");m(d(o,c),t)})})}function E(){let t=[...document.querySelectorAll("[data-set-theme]")];if(!t.length)return;[...new Set(t.map(n=>A(n)))].forEach(n=>M(n))}function g(){E()}function w(t=!0){t?document.addEventListener("DOMContentLoaded",g):g()}export{g as themeBtn,w as themeChange};

index.html

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,19 @@
1919
<h2 class="py-6">Using data-set-theme attribute works everywhere</h2>
2020
<div>
2121
<button class="btn" data-set-theme="">Default</button>
22-
<button class="btn" data-set-theme="dark" data-act-class="btn-active">
22+
<button
23+
class="btn"
24+
data-set-theme="dark"
25+
data-act-class="btn-active"
26+
data-act-attribute="aria-pressed:true"
27+
>
2328
Dark
2429
</button>
2530
<button
2631
class="btn"
2732
data-set-theme="valentine"
2833
data-act-class="btn-active"
34+
data-act-attribute="aria-current"
2935
>
3036
Valentine
3137
</button>
@@ -36,7 +42,12 @@ <h2 class="py-6">Using data-set-theme attribute works everywhere</h2>
3642
>
3743
Caramellatte
3844
</button>
39-
<button class="btn" data-set-theme="abyss" data-act-class="btn-active">
45+
<button
46+
class="btn"
47+
data-set-theme="abyss"
48+
data-act-class="btn-active"
49+
data-act-attribute="title:Active: abyss theme"
50+
>
4051
Abyss
4152
</button>
4253
</div>
@@ -110,6 +121,7 @@ <h2 class="py-6">v2 legacy attributes backward compatibility</h2>
110121
class="btn"
111122
data-toggle-theme="dark,light,valentine"
112123
data-act-class="btn-active"
124+
data-act-attribute="aria-pressed:true"
113125
>
114126
Legacy toggle with act class
115127
</button>

index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
var q="theme";function g(t){return t||q}function u(t){return t||null}function l(t){return(t||"").split(",").map(n=>n.trim()).filter(Boolean)}function E(t,n){if(!t.length)return null;let e=t.indexOf(n||"");return e===-1?t[0]:t[(e+1)%t.length]}function v(t,n){let e=l(t);return e.length>1?E(e,n):u(t)}function x(t,n){let e=l(t);return n?e.length>1?e.includes(n):t===n:t===""}var H=g("");function s(t){return g(t&&t.getAttribute("data-key"))}function w(t,n){t?(document.documentElement.setAttribute("data-theme",t),localStorage.setItem(n,t)):(document.documentElement.removeAttribute("data-theme"),localStorage.removeItem(n))}function h(t){return localStorage.getItem(t)||null}function k(t,n){return s(t)===n}function A(t){return[...document.querySelectorAll("[data-set-theme]")].filter(n=>k(n,t))}var S=new WeakSet;function T(t,n,e,o){n.has(t)||(n.add(t),t.addEventListener(e,o))}function B(t,n){let e=t.getAttribute("data-act-class");e&&(n?t.classList.add(e):t.classList.remove(e))}function m(t,n){A(n).forEach(o=>{if(o instanceof HTMLSelectElement){let a=t||"";o.querySelector(`[value="${a}"]`)&&(o.value=a);return}if(o instanceof HTMLInputElement&&o.type==="checkbox"){o.checked=!!t&&o.value===t;return}let c=o.getAttribute("data-set-theme")||"",r=x(c,t);B(o,r)})}function i(t,n){w(t,n),m(t,n)}function I(t){let n=A(t);if(!n.length)return;let e=h(t);e&&document.documentElement.setAttribute("data-theme",e),m(e,t),n.forEach(o=>{if(o instanceof HTMLSelectElement){T(o,S,"change",function(){i(u(this.value),t)});return}if(o instanceof HTMLInputElement&&o.type==="checkbox"){T(o,S,"change",function(){let c=u(this.value);i(this.checked?c:null,t)});return}T(o,S,"click",function(){let c=this.getAttribute("data-set-theme")||"",r=document.documentElement.getAttribute("data-theme");i(v(c,r),t)})})}function L(){let t=[...document.querySelectorAll("[data-set-theme]")];if(!t.length)return;[...new Set(t.map(e=>s(e)))].forEach(e=>I(e))}var b=new WeakSet;function y(){let t=[...document.querySelectorAll("[data-toggle-theme]")];if(!t.length)return;[...new Set(t.map(e=>s(e)))].forEach(e=>{let o=t.filter(r=>s(r)===e),c=h(e);c&&(document.documentElement.setAttribute("data-theme",c),o.forEach(r=>{let a=r.getAttribute("data-act-class");a&&r.classList.add(a)})),m(c,e),o.forEach(r=>{b.has(r)||(b.add(r),r.addEventListener("click",function(){let a=l(this.getAttribute("data-toggle-theme")),d=document.documentElement.getAttribute("data-theme"),f=E(a,d);i(f,e);let p=this.getAttribute("data-act-class");p&&o.forEach(F=>F.classList.toggle(p))}))})})}function C(){L()}var M=new WeakSet;function D(){let t=[...document.querySelectorAll("select[data-choose-theme]")];if(!t.length)return;[...new Set(t.map(e=>s(e)))].forEach(e=>{let o=t.filter(r=>s(r)===e),c=h(e);c&&(document.documentElement.setAttribute("data-theme",c),o.forEach(r=>{r.querySelector(`[value="${c}"]`)&&(r.value=c)})),m(c,e),o.forEach(r=>{M.has(r)||(M.add(r),r.addEventListener("change",function(){let a=u(this.value);i(a,e),o.forEach(d=>{let f=a||"";d.querySelector(`[value="${f}"]`)&&(d.value=f)})}))})})}function R(t=!0){function n(){C(),y(),D()}t?document.addEventListener("DOMContentLoaded",n):n()}export{R as themeChange};
1+
var k="theme";function A(t){return t||k}function m(t){return t||null}function f(t){return(t||"").split(",").map(n=>n.trim()).filter(Boolean)}function E(t,n){if(!t.length)return null;let e=t.indexOf(n||"");return e===-1?t[0]:t[(e+1)%t.length]}function L(t,n){let e=f(t);return e.length>1?E(e,n):m(t)}function b(t,n){let e=f(t);return n?e.length>1?e.includes(n):t===n:t===""}function y(t){let n=(t||"").trim();if(!n)return null;let e=n.indexOf(":"),o=(e===-1?n:n.slice(0,e)).trim();return o?{name:o,value:e===-1?"":n.slice(e+1)}:null}var $=A("");function s(t){return A(t&&t.getAttribute("data-key"))}function B(t,n){t?(document.documentElement.setAttribute("data-theme",t),localStorage.setItem(n,t)):(document.documentElement.removeAttribute("data-theme"),localStorage.removeItem(n))}function p(t){return localStorage.getItem(t)||null}function I(t,n){return s(t)===n}function C(t){return[...document.querySelectorAll("[data-set-theme]")].filter(n=>I(n,t))}var v=new WeakSet;function S(t,n,e,o){n.has(t)||(n.add(t),t.addEventListener(e,o))}function K(t,n){let e=t.getAttribute("data-act-class");e&&(n?t.classList.add(e):t.classList.remove(e))}function g(t,n){let e=y(t.getAttribute("data-act-attribute"));e&&(n?t.setAttribute(e.name,e.value):t.removeAttribute(e.name))}function l(t,n){C(n).forEach(o=>{if(o instanceof HTMLSelectElement){let a=t||"";o.querySelector(`[value="${a}"]`)&&(o.value=a);return}if(o instanceof HTMLInputElement&&o.type==="checkbox"){o.checked=!!t&&o.value===t;return}let c=o.getAttribute("data-set-theme")||"",r=b(c,t);K(o,r),g(o,r)})}function u(t,n){B(t,n),l(t,n)}function H(t){let n=C(t);if(!n.length)return;let e=p(t);e&&document.documentElement.setAttribute("data-theme",e),l(e,t),n.forEach(o=>{if(o instanceof HTMLSelectElement){S(o,v,"change",function(){u(m(this.value),t)});return}if(o instanceof HTMLInputElement&&o.type==="checkbox"){S(o,v,"change",function(){let c=m(this.value);u(this.checked?c:null,t)});return}S(o,v,"click",function(){let c=this.getAttribute("data-set-theme")||"",r=document.documentElement.getAttribute("data-theme");u(L(c,r),t)})})}function M(){let t=[...document.querySelectorAll("[data-set-theme]")];if(!t.length)return;[...new Set(t.map(e=>s(e)))].forEach(e=>H(e))}var D=new WeakSet;function F(){let t=[...document.querySelectorAll("[data-toggle-theme]")];if(!t.length)return;[...new Set(t.map(e=>s(e)))].forEach(e=>{let o=t.filter(r=>s(r)===e),c=p(e);c&&(document.documentElement.setAttribute("data-theme",c),o.forEach(r=>{let a=f(r.getAttribute("data-toggle-theme"))[0]===c,i=r.getAttribute("data-act-class");i&&r.classList.toggle(i,a),g(r,a)})),l(c,e),o.forEach(r=>{D.has(r)||(D.add(r),r.addEventListener("click",function(){let a=f(this.getAttribute("data-toggle-theme")),i=document.documentElement.getAttribute("data-theme"),d=E(a,i);u(d,e),o.forEach(h=>{let T=f(h.getAttribute("data-toggle-theme"))[0]===d,x=h.getAttribute("data-act-class");x&&h.classList.toggle(x,T),g(h,T)})}))})})}function q(){M()}var w=new WeakSet;function O(){let t=[...document.querySelectorAll("select[data-choose-theme]")];if(!t.length)return;[...new Set(t.map(e=>s(e)))].forEach(e=>{let o=t.filter(r=>s(r)===e),c=p(e);c&&(document.documentElement.setAttribute("data-theme",c),o.forEach(r=>{r.querySelector(`[value="${c}"]`)&&(r.value=c)})),l(c,e),o.forEach(r=>{w.has(r)||(w.add(r),r.addEventListener("change",function(){let a=m(this.value);u(a,e),o.forEach(i=>{let d=a||"";i.querySelector(`[value="${d}"]`)&&(i.value=d)})}))})})}function Z(t=!0){function n(){q(),F(),O()}t?document.addEventListener("DOMContentLoaded",n):n()}export{Z as themeChange};

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

select.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
var v="theme";function s(t){return t||v}function m(t){return t||null}function g(t){return(t||"").split(",").map(e=>e.trim()).filter(Boolean)}function h(t,e){let n=g(t);return e?n.length>1?n.includes(e):t===e:t===""}var I=s("");function a(t){return s(t&&t.getAttribute("data-key"))}function T(t,e){t?(document.documentElement.setAttribute("data-theme",t),localStorage.setItem(e,t)):(document.documentElement.removeAttribute("data-theme"),localStorage.removeItem(e))}function d(t){return localStorage.getItem(t)||null}function x(t,e){return a(t)===e}function A(t){return[...document.querySelectorAll("[data-set-theme]")].filter(e=>x(e,t))}function L(t,e){let n=t.getAttribute("data-act-class");n&&(e?t.classList.add(n):t.classList.remove(n))}function l(t,e){A(e).forEach(o=>{if(o instanceof HTMLSelectElement){let u=t||"";o.querySelector(`[value="${u}"]`)&&(o.value=u);return}if(o instanceof HTMLInputElement&&o.type==="checkbox"){o.checked=!!t&&o.value===t;return}let c=o.getAttribute("data-set-theme")||"",r=h(c,t);L(o,r)})}function p(t,e){T(t,e),l(t,e)}var E=new WeakSet;function S(){let t=[...document.querySelectorAll("select[data-choose-theme]")];if(!t.length)return;[...new Set(t.map(n=>a(n)))].forEach(n=>{let o=t.filter(r=>a(r)===n),c=d(n);c&&(document.documentElement.setAttribute("data-theme",c),o.forEach(r=>{r.querySelector(`[value="${c}"]`)&&(r.value=c)})),l(c,n),o.forEach(r=>{E.has(r)||(E.add(r),r.addEventListener("change",function(){let u=m(this.value);p(u,n),o.forEach(i=>{let f=u||"";i.querySelector(`[value="${f}"]`)&&(i.value=f)})}))})})}function C(t=!0){t?document.addEventListener("DOMContentLoaded",S):S()}export{C as themeChange,S as themeSelect};
1+
var S="theme";function s(t){return t||S}function m(t){return t||null}function g(t){return(t||"").split(",").map(e=>e.trim()).filter(Boolean)}function h(t,e){let n=g(t);return e?n.length>1?n.includes(e):t===e:t===""}function d(t){let e=(t||"").trim();if(!e)return null;let n=e.indexOf(":"),o=(n===-1?e:e.slice(0,n)).trim();return o?{name:o,value:n===-1?"":e.slice(n+1)}:null}var k=s("");function i(t){return s(t&&t.getAttribute("data-key"))}function x(t,e){t?(document.documentElement.setAttribute("data-theme",t),localStorage.setItem(e,t)):(document.documentElement.removeAttribute("data-theme"),localStorage.removeItem(e))}function p(t){return localStorage.getItem(t)||null}function T(t,e){return i(t)===e}function b(t){return[...document.querySelectorAll("[data-set-theme]")].filter(e=>T(e,t))}function L(t,e){let n=t.getAttribute("data-act-class");n&&(e?t.classList.add(n):t.classList.remove(n))}function y(t,e){let n=d(t.getAttribute("data-act-attribute"));n&&(e?t.setAttribute(n.name,n.value):t.removeAttribute(n.name))}function l(t,e){b(e).forEach(o=>{if(o instanceof HTMLSelectElement){let u=t||"";o.querySelector(`[value="${u}"]`)&&(o.value=u);return}if(o instanceof HTMLInputElement&&o.type==="checkbox"){o.checked=!!t&&o.value===t;return}let c=o.getAttribute("data-set-theme")||"",r=h(c,t);L(o,r),y(o,r)})}function v(t,e){x(t,e),l(t,e)}var A=new WeakSet;function E(){let t=[...document.querySelectorAll("select[data-choose-theme]")];if(!t.length)return;[...new Set(t.map(n=>i(n)))].forEach(n=>{let o=t.filter(r=>i(r)===n),c=p(n);c&&(document.documentElement.setAttribute("data-theme",c),o.forEach(r=>{r.querySelector(`[value="${c}"]`)&&(r.value=c)})),l(c,n),o.forEach(r=>{A.has(r)||(A.add(r),r.addEventListener("change",function(){let u=m(this.value);v(u,n),o.forEach(a=>{let f=u||"";a.querySelector(`[value="${f}"]`)&&(a.value=f)})}))})})}function H(t=!0){t?document.addEventListener("DOMContentLoaded",E):E()}export{H as themeChange,E as themeSelect};

src/core.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
normalizeTheme,
44
themeFromSetValue,
55
isSetElementActive,
6+
parseActAttr,
67
} from "./themeLogic.js";
78

89
export const DEFAULT_KEY = keyFromAttr("");
@@ -53,6 +54,16 @@ function setActiveClass(el, active) {
5354
}
5455
}
5556

57+
export function setActiveAttr(el, active) {
58+
const spec = parseActAttr(el.getAttribute("data-act-attribute"));
59+
if (!spec) return;
60+
if (active) {
61+
el.setAttribute(spec.name, spec.value);
62+
} else {
63+
el.removeAttribute(spec.name);
64+
}
65+
}
66+
5667
export function syncSetThemeElements(theme, key) {
5768
const els = getSetThemeElements(key);
5869

@@ -74,6 +85,7 @@ export function syncSetThemeElements(theme, key) {
7485
const value = el.getAttribute("data-set-theme") || "";
7586
const active = isSetElementActive(value, theme);
7687
setActiveClass(el, active);
88+
setActiveAttr(el, active);
7789
});
7890
}
7991

src/themeLogic.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,12 @@ export function isSetElementActive(setValue, theme) {
3434
if (list.length > 1) return list.includes(theme);
3535
return setValue === theme;
3636
}
37+
38+
export function parseActAttr(raw) {
39+
const value = (raw || "").trim();
40+
if (!value) return null;
41+
const i = value.indexOf(":");
42+
const name = (i === -1 ? value : value.slice(0, i)).trim();
43+
if (!name) return null;
44+
return { name, value: i === -1 ? "" : value.slice(i + 1) };
45+
}

0 commit comments

Comments
 (0)