Changelog
Every released version and what actually changed in it. Breaking changes are called out explicitly.
Published to npm as @shakuf-widget/widget. Installing with an explicit version pins it; without one you get the latest.
0.4.0 · awaiting publish
@shakuf-widget/widget@0.4.0 will fail until it is published.
The unversioned URL, which is what the setup guide hands out, keeps
serving 0.3.1 in the meantime and is unaffected. This note comes down
when the release lands.
The "stop animations" control was removing animations rather than stopping them, and on a common pattern that meant the visitor lost the content instead of seeing it hold still. If you rely on that control, upgrade. This release also fixes accessibility defects in the widget's own panel that an October audit found, and an import crash on the npm path. Everything else is additive.
Fixed
-
"Stop animations" could make content disappear. The
rule declared
animation: none, which removes an animation rather than pausing it. Anything whose visible state is produced by an animation therefore reverted to its base state, and for the whole reveal-on-scroll family,.reveal { opacity: 0; animation: fadein 1s forwards }and every AOS-style library, that base state is invisible. Measured: such an element had zero animations and a computed opacity of0that nothing would ever change.
The rule now runs animations to completion instantly, which is the conventional reduced-motion approach, sofill-mode: forwardslands on its final visible value. Verified against the case that matters: an infinite marquee still visibly stops, at the same resting positionanimation: noneleft it.
Animation and transition delays are zeroed too, so a staggered list does not still play out over more than a second after the visitor has asked for stillness. Checked on composite cases, including two staggered animations fighting over one property and a delayed auto-dismiss: the collapsed result is the same computed state the authored sequence ends on, reached at once rather than seconds later. -
A dead declaration in the same rule.
animation-play-state: pausednever had any effect:animationis a shorthand that resets play-state torunning, and it was declared after the longhand in the same block. Removed. -
The coordinator's phone number was not dialable from
abroad. The panel emitted
data-coordinator-phoneverbatim into the href, so055-3000-898producedtel:055-3000-898. The href is now normalised to E.164 (tel:+972553000898) while the link text stays exactly what you typed. Handles a leading00, a number that already carries972, a national trunk zero, both at once, and 8- and 9-digit national numbers.
Numbers with no international form,*6050and1-800-…lines, and anything with an extension or free text typed into the field, are shown as plain text instead of a link. The number is never dropped from the panel. - The Close button's focus ring was invisible. The ring colour and the default accent were the same blue, so on the panel header it measured 1.00:1. Close is the first Tab stop in the panel. The ring there now uses the header's own text colour, and the launcher's ring is two-tone, so it stays visible on any host background.
- English panels showed Hebrew section headings. The five group titles were computed once when the module loaded, before the language was resolved, so every English install read "טקסט", "צבע וניגודיות" and the rest in an English voice. Switching language at runtime could not repair it. They are now resolved at render time.
-
"Hide images" removed alt text from screen readers.
It used
visibility: hidden, which drops an element from the accessibility tree: informative alt text vanished and image-only links lost their names. It now usesopacity: 0, which hides the pixels and keeps the layout and the tree. Video is no longer hidden by this control, since an invisible video would leave its controls focusable; stopping motion is what "stop animations" is for. - The panel could run off short screens. Its height ignored the distance from the screen edge, so below roughly 490px of viewport height, landscape phones and 400% zoom, either the title and Close or the Reset button and the disclaimer were cut off. The height is now capped to the space available, and on very short screens the whole panel scrolls.
-
Importing the npm package crashed outside a browser.
import '@shakuf-widget/widget'threw aReferenceErrorduring server-side rendering in Next, Nuxt or Remix, and in Node test runners, beforemount()was ever called. Importing now does nothing on its own, as documented. -
Closing the panel stole focus. When the panel was
opened from a control on your own page, such as a footer link calling
window.shakuf.open(), focus returned to that control and was then moved again to the launcher. It now stays where it belongs. -
"Stop animations" missed smooth scrolling set on
html. The rule matched elements insidebodyonly, andhtmlis the most common placescroll-behavior: smoothis set. -
Two host-page focus rings were being hidden. High
contrast removed every
box-shadow, which is how most design systems draw focus, and "highlight links" gave every link the same outline, so the focused one looked like its neighbours. High contrast now adds a yellow focus outline at 16.6:1, and a focused link gets a visibly heavier ring. -
Smaller fixes. A landmark with a role such as
constructorno longer prints function source as its name in the landmarks list, and text sizing no longer keeps elements the page has removed in memory.
Changed
- The panel's "details missing" message is worded more carefully. It used to state that Israeli law requires every site to publish an accessibility statement and appoint a coordinator. The coordinator duty in particular depends on the site owner, so the message now says the law requires many sites to do both. The same change was made in the README, the setup guide and the disclaimer.
-
The panel's attribution links no longer send a
referrer. They now carry
noreferrer, so clicking one does not tell the destination which page it came from. The widget collects nothing, and its links should not either.
Added
-
html[data-shakuf-motion="off"]is now documented as a public, stable selector. A page cannot makeprefers-reduced-motion: reduceevaluate true from script, so every@media (prefers-reduced-motion: reduce)block a site has written stays inert when a visitor uses our control instead of their OS setting. The better a site's reduced-motion work, the worse that was.
A real case: a client-logo marquee is a 7500px track with five copies of the list inside anoverflow: hiddenwindow with an edge mask, and its authored reduced-motion state unwraps the track, drops the duplicates and removes the mask. Held still without the rest, it reads as a stalled carousel, which is precisely what that media query exists to prevent.
Repeating the still-state under our attribute is enough to fix it: our rule only touchesanimation-*,transition-*andscroll-behavior, so a site's own layout declarations sit alongside ours untouched. There is a copy-paste recipe in the setup guide. -
data-motion-exclude, a CSS selector for elements "stop animations" must leave alone, including their descendants. A marquee track and its children are one unit, and an exclusion stopping at the element itself would miss the point.
This is for the narrower case the recipe above cannot cover: a still-state that needs to own the animation itself, such as slowing one rather than stopping it. Our declarations are!important, and not matching in the first place is the only thing that reliably overrides them. Most sites do not need this.
An exclusion is a trade, not an improvement: an excluded element stops responding to the control entirely unless your own CSS handles it. Invalid selectors, and any value containing braces, are rejected with a console warning and treated as absent. - Windows High Contrast support in the panel. On and off were shown only by background colour, which forced-colours mode removes, so every toggle looked the same. The switches now use system colours.
- The widget no longer prints. The launcher used to appear on every printed page.
- A licence and disclaimer banner opens both bundles. Self-hosted copies now carry the version, the Apache-2.0 notice and the one-line disclaimer.
- The npm page now shows the README, and the package names its author.
Note for installers
-
jsDelivr is a third party on your site. The widget
still makes no network requests of its own, but the visitor's browser
fetches the script from
cdn.jsdelivr.net, which exposes their IP address and user-agent to it. That is true of any CDN-hosted library and is worth stating plainly: if your privacy policy enumerates third parties by name, jsDelivr belongs on that list. The setup guide now has self-hosting instructions for sites that would rather not add one, which also answer sites that need Subresource Integrity and therefore a pinned version.
Internal
-
Sourcemaps are now published. Both bundles have always
ended with a
sourceMappingURL, but the maps were excluded from the package, so every installer who opened devtools on a page running the widget got a 404 forshakuf.js.map. They were excluded because tsup embeds the original source in the map and the repository was private; it has been public since August, so that reason no longer exists.
The package grows from 54.6 kB to 158.2 kB as a result. Visitors pay none of it: a browser requests a.maponly when devtools are open, jsDelivr serves each file on request, and the bundle itself does not include them. What you get for it is a readable stack trace instead of one line of minified code. - The bundle is smaller despite the fixes. Comments in the panel stylesheet were shipping to every visitor, because they sit inside a template string the minifier cannot touch. They are now stripped at build time, the same way the host stylesheet's already were. The bundle went from 15.95 KB to 15.02 KB gzipped.
0.3.1 · 14 August 2026
One fix, and it affects far more installs than the report that found it. If your site ships a CSS reset, and most do, upgrade.
Fixed
-
A CSS reset on the host page could switch off the widget's
stacking entirely, hiding the launcher behind site overlays.
The widget's
positionandz-indexwere declared in a:hostrule. Per the CSS Scoping specification, a:hostrule loses to any rule in the host document that matches the host element. Not on specificity, categorically. Measured:* { position: static; z-index: auto }, the weakest selector that exists, erased both. The launcher then painted in DOM order and vanished behind anything with az-index.
It was reported as "the bundle sets no z-index". It always did, in the one place a stylesheet erases for free.
The worst version of this is a blocking overlay: a visitor pinned behind a forced-update or offline screen is exactly who may need stop-animations or high contrast, and that is precisely when the button disappeared. Reproduced withelementFromPointagainst an opaque overlay atz-index: 10000, and confirmed fixed.
Both properties are now written inline on the host element, which normal host rules cannot override. Deliberately not!important: a site that means to reposition us can still win with its own!important, so existing workarounds keep working.
Note for host pages
-
Never give
#shakuf-rootatransform,filter,perspective,containorwill-change. Any of those makes it a containing block for the fixed-position launcher inside it, which silently moves the button somewhere unintended: no error, no obvious cause. A plainposition: relativeis safe and does not do this.
0.3.0 · 14 August 2026
Two additions for sites adopting the widget, both requested by an integration. Nothing breaks: every existing option keeps its meaning.
Added
-
Logical placement.
data-positionnow also acceptsbottom-start,bottom-end,top-startandtop-end. These follow reading direction, so the launcher moves to the other side when the page switches between Hebrew and English, the same way a site built on CSS logical properties moves everything else. A site with its own floating button in the opposite logical corner could not avoid a collision with the physical values: whichever corner it picked, the two met in one of its two languages. The physical values are unchanged. -
A preferences import path.
window.shakuf.getPrefs()returns the visitor's current settings;setPrefs()replaces them and applies the result. Bundler hosts can also passmount({ initialPrefs }), which seeds settings only for visitors who have none stored here yet.
This exists so a site replacing another accessibility tool does not silently reset everyone who had set large text or high contrast, which is exactly the population the feature is for. It also lets a site restore settings from a profile of its own, so a preference is not stranded on one device.
We chose to expose this rather than document thelocalStoragekey as a supported contract. Freezing an internal storage format to serve a one-time migration means every later change breaks a consumer we cannot see.
Note for anyone storing preferences
-
The widget still transmits nothing and still has no server. If you save
getPrefs()output to a user profile, that is your decision under your own privacy policy, and worth making deliberately, since values like "high contrast" or "dyslexia-friendly font" can be read as an inference about disability. That is why we do not touch them.
Internal
- CSS comments are stripped from the bundle at build time. They lived inside a template literal, so the minifier could not reach them and they were shipping to every visitor: 44% of the injected stylesheet. No behaviour change; the bundle dropped from 16.99 KB to 15.43 KB gzipped, which is what made room for the two features above.
0.2.1 · not released
@shakuf-widget/widget@0.2.1 will fail. All three fixes below
ship in 0.3.0. Upgrade to that or later. The entry is
kept because the fixes are real and worth reading about.
Three display bugs, each of which broke a feature for exactly the people it exists for. If you are on 0.1.0–0.2.0, upgrade.
Fixed
-
Inverted colours, greyscale and both saturation levels did
nothing on their own. The four settings are composed into one
filterdeclaration through custom properties, and the fallback for an unset half wasnone. That is a legal value for the property but not a legal item in a filter list, so any single setting producedfilter: invert(1) none: invalid, and dropped altogether. Turning on two at once worked, which is why it went unnoticed. The panel reported the setting as active throughout, so a visitor who could not see the page had no way to tell it had done nothing. The fallbacks are now identity functions. -
Links were invisible in high-contrast mode. The rule
that forces text white outranked the rule that colours links yellow:
:not(svg):not(svg *)each add their argument's weight, so the sweeping rule computed higher than the link rule that followed it. Links rendered#fff, indistinguishable from body text, in the one mode built for low-vision users. The type exclusions moved inside:where(); the ID exclusion stayed out of it, so the palette still overrides host stylesheets that use their own!important. -
The launcher button was always tagged Hebrew. Its
label is translated but its
langattribute was hardcoded, so on an English page a screen reader read an English label in a Hebrew voice. It now resolves the language like the panel and the announcement region already did, and follows a runtime language change.
0.2.0 · 13 August 2026
Added
-
English. The widget reads the page language from
<html lang>and renders accordingly, including direction.data-langoverrides it. Hebrew remains the default, so existing installs are unchanged. - Language changes are followed without a reload. Apps that switch language at runtime switch the widget too, including the screen-reader announcement region: a live region left tagged with the wrong language is read aloud in the wrong voice.
-
data-mount: a CSS selector for the element to mount into, instead of<body>. Required for apps that markbodychildreninertbehind a full-screen blocker, which would otherwise disable the widget at exactly the moment a visitor is stuck looking at one. -
window.shakuf: host-page control,open,close,toggle,hide,show,hidden,reset,setLanguage,lang,destroy. -
data-hidden: start with the launcher hidden, for hosts that already know the visitor turned it off. Avoids painting the button and then removing it on every load. -
shakuf:readyevent ondocument. It fires during mount, beforewindow.shakufexists, so the widget arrives inevent.detail.
Fixed
- One failing feature disabled the rest. The apply calls were unguarded, so a single throw ended the loop. On load that meant the visitor's other saved preferences silently never applied; on a click it meant the save and the panel refresh never ran, leaving the control looking dead. Each feature is now isolated.
- A failed change was announced as a success. The panel announced the new state immediately after requesting it, whether or not it had taken effect, telling the one person who cannot see the screen that something happened when it had not. Announcements now describe what actually occurred, and a failure says so.
- Direction was hardcoded. The widget's own stylesheet fixed writing direction to RTL, and the toggle knob slid the wrong way in English.
Breaking
-
On the
A11yWidgetclass,show()used to open the panel.open()now opens the panel andshow()shows the launcher, matching the global API. Only affects consumers using the class directly via npm. - The size budget rose from 15 KB to 17 KB gzipped. The bundle ships both languages, so a Hebrew-only install carries the English strings: the cost of keeping one script tag with no language in the URL.
0.1.1 · 12 August 2026
Documentation only. The bundle is identical to 0.1.0.
-
NOTICEshipped with an unfilled copyright line, so a redistributor could not have complied with section 4(d) of the licence even if they wanted to. - The disclaimer cited licence section 9 for the limitation of liability. Section 9 is Accepting Warranty or Additional Liability; the limitation is section 8.
-
A broken link to
SECURITY.md, which is not part of the package and so returned 404 for anyone reading the published copy.
0.1.0 · 12 August 2026
First release. Hebrew only.
- Display-preference panel: text size, line/letter/word spacing, readable font, unjustify, contrast, saturation, link highlighting, hide images, stop animations, focus highlighting, large cursor and a reading ruler.
- Navigation lists for headings, landmarks and links, rendered in the widget's own panel, never by modifying the host page.
-
Every change is visitor-initiated, reversible in one click, and stored
only in the browser's
localStorage. No server, no account, no data collection. - Shadow DOM with full containment, so the widget cannot leak into the host page or be broken by it.