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.

The rest of this site is in Hebrew. This page is in English because it is written for the people installing the package.

0.4.0 · awaiting publish

Not on npm yet. Installing @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 of 0 that nothing would ever change.
    The rule now runs animations to completion instantly, which is the conventional reduced-motion approach, so fill-mode: forwards lands on its final visible value. Verified against the case that matters: an infinite marquee still visibly stops, at the same resting position animation: none left 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: paused never had any effect: animation is a shorthand that resets play-state to running, 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-phone verbatim into the href, so 055-3000-898 produced tel:055-3000-898. The href is now normalised to E.164 (tel:+972553000898) while the link text stays exactly what you typed. Handles a leading 00, a number that already carries 972, a national trunk zero, both at once, and 8- and 9-digit national numbers.
    Numbers with no international form, *6050 and 1-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 uses opacity: 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 a ReferenceError during server-side rendering in Next, Nuxt or Remix, and in Node test runners, before mount() 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 inside body only, and html is the most common place scroll-behavior: smooth is 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 constructor no 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 make prefers-reduced-motion: reduce evaluate 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 an overflow: hidden window 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 touches animation-*, transition-* and scroll-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 for shakuf.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 .map only 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 position and z-index were declared in a :host rule. Per the CSS Scoping specification, a :host rule 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 a z-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 with elementFromPoint against an opaque overlay at z-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-root a transform, filter, perspective, contain or will-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 plain position: relative is 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-position now also accepts bottom-start, bottom-end, top-start and top-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 pass mount({ 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 the localStorage key 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

This version was never published to npm. Installing @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 filter declaration through custom properties, and the fallback for an unset half was none. That is a legal value for the property but not a legal item in a filter list, so any single setting produced filter: 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 lang attribute 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-lang overrides 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 mark body children inert behind 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:ready event on document. It fires during mount, before window.shakuf exists, so the widget arrives in event.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 A11yWidget class, show() used to open the panel. open() now opens the panel and show() 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.

  • NOTICE shipped 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.