# Shayach: technical reference for AI agents and developers

This is the complete technical reference for Shayach: a free,
open-source accessibility widget, hosted at shayach.co.il, plus a
generated accessibility statement
(הצהרת נגישות) for Israeli websites. One script tag adds a customizable
accessibility button and panel (Hebrew, Arabic, Russian, English) and can
publish a statement page generated from the site's configuration. A weekly
agent pipeline tracks Israeli accessibility law (Regulation 35, IS 5568) and
ships updates through a versioned URL, so every site stays current without
touching its code.

Other documents:

- [README.md](../README.md): the Hebrew overview for site owners and decision
  makers.
- [docs/guide.md](guide.md): a Hebrew step-by-step guide for site owners with
  no technical background (per-platform install instructions, the statement,
  troubleshooting). Point site owners there.
- [llms.txt](https://shayach.co.il/llms.txt): a short summary of
  this document for AI agents.
- [docs/maintainers.md](maintainers.md): compliance automation and releasing,
  for maintainers of this repository.

Everything here matches the code. If this document and the JSON Schema at
`https://shayach.co.il/v1/config.schema.json` ever disagree, the
schema wins.

## Contents

- [No immunity. Read this first.](#no-immunity-read-this-first)
- [Agent procedure](#agent-procedure)
- [Do not](#do-not)
- [Install](#install)
- [Configuration](#configuration)
- [Accessibility statement](#accessibility-statement)
- [Exemptions](#exemptions)
- [Content Security Policy](#content-security-policy)
- [What it does](#what-it-does)
- [What it does not do](#what-it-does-not-do)
- [Standards: WCAG 2.2 AA](#standards-wcag-22-aa)
- [Privacy, footprint and performance](#privacy-footprint-and-performance)
- [Versioning and updates](#versioning-and-updates)
- [License](#license)

## No immunity. Read this first.

**This widget does not protect a site from accessibility lawsuits. Not fully,
not partially, not at all.** No accessibility widget does. If anyone says
their widget makes a site "fully compliant", "lawsuit-proof" or comes with
"legal protection", that is marketing, not law. Don't trust it.

Israeli law (Regulation 35) looks at the website itself: does it meet IS 5568
at level AA, and does it publish an accurate accessibility statement. A widget
cannot fix inaccessible markup, missing alt text, poor contrast or forms that
don't work with a keyboard.

The Commission for Equal Rights of Persons with Disabilities says the same in
its official FAQ
([gov.il](https://www.gov.il/he/pages/website_accessibility_faq), "האם אני
חייב להתקין באתר שלי 'סרגל נגישות'?"):

> יש להדגיש כי שילובם של סרגלים אלו אינו פוטר מחובת העמידה בכל דרישות
> התקנות והתקן האמורים לעיל ביחס לאתר אינטרנט, או מביצוע כל התאמות הנגישות
> הנדרשות. עוד יודגש, כי אין כל חובה להשתמש ב"סרגל נגישות" על מנת להגיע לרמת
> הנגשה הנדרשת בחוק ובתקנות.

In short: adding a toolbar does not exempt a site from any requirement, and
the law does not require a toolbar at all.

What this project actually does is **help**:

- It gives visitors adjustments that make the site easier for them to use.
- It generates an accessibility statement that follows the official template
  and updates its wording when the requirements change, to reduce the risk
  that a missing or outdated statement is the reason the site gets a
  complaint.
- It gives people a clear way to report accessibility problems to the site
  owner, so the owner hears about them and can fix them before they turn into
  claims.

The site owner is still responsible for the site's accessibility: build it
accessibly (see [What it does not do](#what-it-does-not-do)) and have it
reviewed by an accessibility professional. When you install the widget for an
owner, tell them this in plain words.

This project is not legal advice.

## Agent procedure

Follow these steps when installing or updating the widget in a project.

1. **Read** this document in full and the config JSON Schema
   (`https://shayach.co.il/v1/config.schema.json`).
2. **Inspect the site's stack.** Find the one place that renders on every
   page: the root layout or document (`app/layout.tsx`, `pages/_document.tsx`,
   `index.html`, `nuxt.config.ts`, an Astro base layout, a CMS theme footer).
   See [Frameworks and platforms](#frameworks-and-platforms). Check whether
   the site sends a Content Security Policy (response headers or a CSP
   `<meta>` tag).
3. **Ask the owner only for facts only they know.** These are the statement
   details: the organization's name, the people to contact about
   accessibility (name, phone, email), whether there is a physical place that
   serves the public and how it is accessible, which accessibility work was
   actually done on the site, and whether the site was ever audited (and at
   which level). Take brand colors, the site language and URLs from the
   project itself. Use today's date for `updatedAt` only when the owner
   reviews the statement with you today.
4. **Generate the config and the tag.** Build `window.A11yConfig` (see
   [Configuration](#configuration)) and add the config script and the widget
   script once, so they load on every page (see [Install](#install)).
5. **Add the statement page and the footer link.** Create a statement page
   with the container and `statement.js` (see
   [Publish the statement](#publish-the-statement)), set `statementUrl` to it
   on every page, and add a footer link named "הצהרת נגישות" (or the same name
   in the page language) on every page. Both are required: at least two ways
   to reach the statement.
6. **Verify in a browser** (a real one or a headless one):
   - the round accessibility button is visible in the expected corner after
     the page loads;
   - clicking it opens the panel;
   - an adjustment changes the page (for example, open the "Text adjustments"
     card, increase font size and check that text grows, or turn on dark
     contrast in the "Color adjustments" card);
   - the console shows no errors, and no `[shayach]` warnings you did not
     expect (each warning names the config field it ignored);
   - the statement page renders the statement, with no "missing details" list
     at the top unless the owner has not provided those details yet;
   - if the site has a CSP, nothing is blocked (see
     [Content Security Policy](#content-security-policy)).
7. **Tell the owner** what you installed, what details are still missing from
   the statement, and that the widget gives no legal protection and does not
   replace fixing the site itself. Point them to
   [docs/guide.md](guide.md) for the Hebrew explanation.
8. **Updates:** to check for updates later, read the changelog
   ([CHANGELOG.md](../CHANGELOG.md) or
   `https://shayach.co.il/v1/changelog.json`) from the version the
   project last saw. Apply every `[Action required]` entry; nothing else needs
   work, because every `1.x` release reaches the site automatically.

## Do not

- **Never claim compliance.** Never tell an owner, write in a commit, a page
  or a statement that the widget makes the site compliant, legal, accessible,
  "fully accessible" or protected from lawsuits.
- **Never invent statement facts.** No made-up names, phone numbers, emails,
  dates, adjustments, measures, tested browsers or assistive technologies.
  Missing required details are listed at the top of the statement until the
  owner provides them; that is the intended behavior.
- **Never set `conformanceLevel` without an audit.** Set it only when the site
  was actually audited at that level, and only with the owner's confirmation.
- **Never decide that an owner is exempt.** Report the
  [exemption](#exemptions) facts and send the owner to the Commission or a
  lawyer.
- **Never hide the no-immunity notice from the owner.** When you summarize the
  install, say that the widget does not give legal protection and does not
  replace making the site accessible.
- Don't load `widget.js` more than once per page, don't self-host a copy (you
  would stop receiving updates), and don't pin a full version: use the `/v1/`
  URL.
- Don't install it next to another accessibility widget or plugin; remove
  the other one first.
- Don't style or remove the widget's own elements (`#shayach`,
  `#shayach-overlay`).

## Install

Add the config (optional) and the script once, before `</body>`, on every page
(in a framework: the root layout):

```html
<script>
  window.A11yConfig = {
    icon: { shape: "circle" },
    theme: { primary: "#1f5eff" },
    // optional: the URL of your accessibility statement page, if you have one
    statementUrl: "/accessibility-statement"
  };
</script>
<script src="https://shayach.co.il/v1/widget.js" defer></script>
```

To also publish the accessibility statement page, see
[Accessibility statement](#accessibility-statement).

Nothing else is needed. The widget waits until the page has loaded, then adds
one button in its own Shadow DOM. Until a visitor uses it, it adds nothing to
the page's `<head>`, makes no other requests, and writes no storage.

Details:

- **Placement:** before `</body>` is recommended. With `defer` it also works
  in `<head>`.
- **`defer`:** keep it. The script never blocks rendering.
- **Config timing:** `widget.js` reads `window.A11yConfig` once, at
  `DOMContentLoaded` (or immediately if the script runs after that). Any
  inline config script in the page's HTML is therefore picked up, wherever it
  is. A config set later, for example from a framework component after
  hydration, is not picked up. The simple rule: keep the config in a plain
  inline script placed before the widget script.
- **When the button appears:** after the page's `load` event and the next idle
  moment, or after 3 seconds, whichever comes first.
- **Returning visitors:** if the visitor has saved adjustments, the widget
  loads the panel code (`app.js`) right away to re-apply them.
- **Loaded twice by mistake:** a second copy does nothing.
- **Single-page apps:** load it once in the root layout, not per route. The
  button survives client-side navigation, including routers that replace the
  whole `<body>` (Turbo Drive, some SPA routers). Adjustments also apply to
  content added later. The statement page is different: `statement.js`
  renders only the containers present when it first runs (see
  [Publish the statement](#publish-the-statement)).
- **What it adds to the DOM:** `<div id="shayach">` (the button and
  panel, in an open shadow root) at the end of `<body>`, and
  `<div id="shayach-overlay">` only while a reading guide or reading mask is
  on. One global: `window.A11yConfig`, which the site defines.
- **Where it loads from:** `app.js`, font files and statement chunks load from
  the same `/v1/` directory as `widget.js`, only when needed.

### Frameworks and platforms

**Next.js (App Router).** In `app/layout.tsx`, inside `<body>`, with
`next/script`:

```tsx
import Script from "next/script";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="he" dir="rtl">
      <body>
        {children}
        <Script id="shayach-config" strategy="beforeInteractive">
          {`window.A11yConfig = { statementUrl: "/accessibility-statement" };`}
        </Script>
        <Script src="https://shayach.co.il/v1/widget.js" strategy="afterInteractive" />
      </body>
    </html>
  );
}
```

The config uses `beforeInteractive` so it exists before the widget runs; the
widget uses `afterInteractive` (or `lazyOnload`). Don't use
`strategy="worker"`: the widget needs the main thread and the DOM.

**Next.js (Pages Router).** Put the same two `<Script>` elements in
`pages/_document.tsx` (`beforeInteractive` is only allowed there) or put the
config in `_document.tsx` and the widget `<Script>` in `pages/_app.tsx`.

**React (Vite, Create React App) and other client-rendered apps.** Paste the
plain HTML snippet before `</body>` in `index.html` (Vite: the project root;
Create React App: `public/index.html`). Don't mount the script from a
component.

**Vue (Vite).** Paste the plain HTML snippet into `index.html`.

**Nuxt 3 and 4.** In `nuxt.config.ts`:

```ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        { innerHTML: 'window.A11yConfig = { statementUrl: "/accessibility-statement" };', tagPosition: "bodyClose" },
        { src: "https://shayach.co.il/v1/widget.js", defer: true, tagPosition: "bodyClose" },
      ],
    },
  },
});
```

**Astro.** In the base layout, before `</body>`, mark both scripts
`is:inline` so Astro does not bundle them:

```astro
<script is:inline>
  window.A11yConfig = { statementUrl: "/accessibility-statement" };
</script>
<script is:inline src="https://shayach.co.il/v1/widget.js" defer></script>
```

**SvelteKit:** `src/app.html` before `</body>`. **Angular:** `src/index.html`
before `</body>`. **Plain HTML:** every page, before `</body>` (or a shared
footer include).

**WordPress, Wix, Shopify, Webflow, Squarespace:** use the platform's
site-wide footer code setting. Step-by-step instructions (in Hebrew) for each
are in [docs/guide.md](guide.md). In short: WordPress with a header and
footer code plugin (for example WPCode); Wix: Settings, Custom Code, Body end
(premium plan with a connected domain); Shopify: `theme.liquid` before
`</body>`; Webflow: Site settings, Custom code, Footer code (paid site plan);
Squarespace: Code Injection, Footer (plan with code injection). Caveats:
Shopify theme code does not reach the checkout pages; WordPress.com runs
scripts only on a plan that allows plugins; with Elementor, use its HTML
widget for the statement container. Remove any other accessibility widget or
plugin first: two accessibility buttons confuse visitors. After installing on
WordPress, clear the caching plugin's cache (for example WP Rocket or
LiteSpeed Cache).

**Google Sites** does not allow site-wide scripts, so the widget cannot be
added there.

## Configuration

All options are optional. The full machine-readable definition is the JSON
Schema at `https://shayach.co.il/v1/config.schema.json`. Invalid
values never break the page: they are ignored and reported in the browser
console with the prefix `[shayach]`, as the option's path, the value and what
the widget used instead (for example `[shayach] icon.size: 120 is not valid;
using 96.`). The allowed values are in the tables below and in the schema.

Top-level keys of `window.A11yConfig`: `language`, `icon`, `theme`,
`statementUrl`, `statement`. Unknown keys at any level are ignored with a
warning.

| Option | Type | Default | Notes |
|---|---|---|---|
| `language` | `"auto"`, `"he"`, `"ar"`, `"ru"`, `"en"` | `"auto"` | `auto` uses the page's `lang` attribute (a region such as `he-IL` is fine; `iw` counts as Hebrew), otherwise Hebrew. A visitor's own choice in the panel always wins. |
| `statementUrl` | URL | none | Link to the statement page. The panel footer's "הצהרת נגישות" links to it (new tab). The about card's button opens the statement window when `statement` is set, and the window then also links to this page (new tab); without `statement` the about card's button is a plain link to this page, because the widget has no statement data to show. |
| `statement` | object | none | Details for the generated accessibility statement. See [Accessibility statement](#accessibility-statement). `null` counts as not set. When set, the panel's about card opens it in the statement window. |
| `icon.shape` | `"circle"`, `"rounded"`, `"pill"`, `"edge-tab"` | `"circle"` | `pill` and `edge-tab` show a text label. |
| `icon.position` | `"auto"`, `"top-right"`, `"top-left"`, `"upper-right"`, `"upper-left"`, `"middle-right"`, `"middle-left"`, `"lower-right"`, `"lower-left"`, `"bottom-right"`, `"bottom-left"` | `"auto"` | `auto` is bottom-right on RTL pages, bottom-left on LTR pages. `upper` and `lower` sit a quarter and three quarters of the way down (`top: calc(offsetY + (100% - 2 * offsetY - size) * 0.25)`, or `0.75`). The four `upper`/`lower` values need widget 1.6.0: an older cached loader warns and uses the default. |
| `icon.offsetX`, `icon.offsetY` | number, 0 to 200 | `20` | Distance from the edges in px. For the middle positions, `offsetY` shifts the button down from the vertical center; for `upper` and `lower` it is the margin kept at the top and bottom of the screen. `edge-tab` ignores `offsetX`. |
| `icon.size` | number, 44 to 96 | `56` | Button height in px. |
| `icon.background`, `icon.foreground` | CSS color | `theme.primary`, best of black/white | A foreground below 4.5:1 contrast is replaced automatically. |
| `icon.labelText` | string, up to 40 characters | the word for accessibility in the panel language | Label for `pill` and `edge-tab`. Longer text is cut to 40 characters. |
| `icon.labelLanguage` | `"he"`, `"ar"`, `"ru"`, `"en"` | follows the panel language | Fixes the built-in label's language. |
| `icon.mobile` | `{ position, offsetX, offsetY, size }` | same as desktop | Applies on screens up to 640px wide. `position` here has no `auto`. |
| `icon.draggable` | boolean | `true` | Visitors can drag the button (it snaps to the nearest of the ten positions) or choose a position in the panel's "Button position" card, which also works with a keyboard. The choice is stored in the visitor's browser (`shayach:v1`, per origin: `www` and the bare domain are separate), wins over `icon.position` and `icon.mobile.position` on every screen size (your offsets and size still apply), and Reset settings removes it. `false` keeps the button where you put it, hides the card, and ignores (but keeps) a stored choice. |
| `theme.primary` | CSS color | `#1f5eff` | Header, selected tiles, tile icons. |
| `theme.onPrimary` | CSS color | best of black/white | Text on `primary`. |
| `theme.background`, `theme.text`, `theme.muted`, `theme.border`, `theme.surface` | CSS color | `#ffffff`, `#1d1d1f`, `#5f6672`, `#d5dbe7`, `#f3f6ff` | Panel colors. Text colors below 4.5:1 contrast are replaced automatically, so the panel always meets WCAG 2.2 AA. |
| `theme.scrollbar` | CSS color | `theme.primary` | Scrollbar thumb inside the panel and the statement window (the track is `theme.surface`). Below 3:1 against `theme.surface` (WCAG 1.4.11) it is replaced with `theme.text`, with a warning. |

Validation rules:

- Numbers outside their range are clamped to the nearest allowed value (and
  rounded), with a warning. A non-number is ignored.
- URLs (`statementUrl`) must be `http(s)` or relative; anything else (for
  example `javascript:`) is ignored.
- Colors must be valid CSS colors (hex, `rgb()`, `hsl()`, named colors). An
  invalid color falls back to the default.

The button always shows the universal accessibility symbol, in one of the
four shapes. The built-in label is "נגישות" (Hebrew), "إمكانية الوصول"
(Arabic), "Доступность" (Russian) or "Accessibility" (English).

### Theme contrast auto-correction and warnings

The widget guarantees WCAG 2.2 AA text contrast inside its own UI, whatever
colors the site chooses:

- `theme.text` and `theme.muted` below 4.5:1 on `theme.background`,
  `theme.onPrimary` below 4.5:1 on `theme.primary`, and `icon.foreground`
  below 4.5:1 on the icon background are replaced with black or white
  (whichever contrasts more), with a warning that states the measured ratio.
- `theme.surface` is painted under text when a tile or button is hovered. If
  `theme.text` on it is below 4.5:1, the surface is replaced with the default
  surface (or the panel background).
- If `theme.primary` on `theme.background` is below 3:1, a warning says the
  tile icons will be hard to see. That color is not replaced.
- `theme.scrollbar` (or `theme.primary` when it is not set) below 3:1 against `theme.surface` is replaced with `theme.text`, with a warning.
- Colors whose contrast cannot be measured (CSS variables, keywords such as
  `currentColor`, translucent colors) are replaced with the default, with a
  warning. Use solid hex, `rgb()`, `hsl()` or named colors.

The panel's colors are resolved by `app.js`, so these warnings appear in the console when the panel first opens, not on page load. The button's own colors are corrected by `widget.js` before the button appears, without a warning at that point; the `icon.foreground` warning is logged with the others when the panel first opens.

A dark panel, for example:

```js
window.A11yConfig = {
  theme: {
    primary: "#8ab4ff",
    background: "#16181d",
    text: "#f2f4f8",
    muted: "#b4bccb",
    border: "#3a404c",
    surface: "#232833"
  }
};
```

A pill-shaped button with a custom label on the middle of the left side, and a
smaller button at the bottom on phones:

```js
window.A11yConfig = {
  icon: {
    shape: "pill",
    labelText: "נגישות",
    position: "middle-left",
    mobile: { position: "bottom-left", size: 48 }
  }
};
```

A button that visitors cannot move, a quarter of the way down the right side,
with a different scrollbar color in the panel:

```js
window.A11yConfig = {
  icon: { position: "upper-right", draggable: false },
  theme: { primary: "#B93A22", scrollbar: "#3B2032" }
};
```

## Accessibility statement

Israeli law requires a website to publish an accessibility statement (הצהרת
נגישות). This project generates the statement text in Hebrew, Arabic, Russian
and English from the `statement` config. The text follows the Commission's
official template and the confirmed requirements in
[`compliance/legal-state.json`](../compliance/legal-state.json), and is kept
current by the compliance agents. It cites IS 5568, which is based on WCAG 2.0.
A generated statement is text only: it does not make a site accessible and
gives no legal protection.

### Publish the statement

When `statement` is set, the panel shows the generated statement in a window inside the widget (a modal dialog over the page, with fact boxes, the contact details first, one block per section of the official template, and a print button that prints only the statement). The about card's "להצהרת הנגישות המלאה" opens it; the footer's "הצהרת נגישות" links to `statementUrl` when set and otherwise opens the same window. The statement describes the widget's panel, so `widget.js` must be installed on the site: `statement.js` alone does not add the panel.

The statement window alone is not enough. The Commission's guide asks for at
least two ways to reach the statement, and the regulation asks for a prominent
place, so publish a statement page and link it from the footer and from the
widget (`statementUrl`). Create a statement page with this container and
script, using the same `window.A11yConfig`:

```html
<script>
  window.A11yConfig = {
    statementUrl: "/accessibility-statement",
    statement: {
      organizationName: "Example Ltd",
      updatedAt: "2026-10-02",
      adjustments: ["All images have text alternatives", "Every page can be used with a keyboard"],
      coordinator: { name: "Dana Levi", phone: "03-1234567", email: "access@example.com" },
      noPhysicalService: true,
      knownLimitations: ["Videos in the archive have no captions"]
    }
  };
</script>
<div data-shayach-statement></div>
<script src="https://shayach.co.il/v1/statement.js" defer></script>
```

Then set `statementUrl` to that page on every page, so the widget links to it.
The simplest setup keeps one `window.A11yConfig` (with `statement` and
`statementUrl`) in the site-wide snippet, and puts only the container and
`statement.js` on the statement page.

**At least two ways to reach the statement are required.** The Commission's
guide says the statement must be reachable in at least two ways, with
consistent link names. Use the widget's panel link (`statementUrl`) plus a
link in the footer of every page, both named "הצהרת נגישות" (or the same name
in the page language).

Container attributes (all optional):

- `data-lang`: `he`, `ar`, `ru` or `en`. Without it, the statement uses the
  `lang` of the nearest ancestor with a `lang` attribute, then the page's
  `lang`, otherwise Hebrew.
- `data-heading-level`: a whole number from 1 to 4, the level of the
  statement's title; section headings are one level lower. Default `2`. Any
  other value falls back to `2`. (In the statement window the title is an `h2` and sections are `h3`.)
- `data-layout`: `blocks` renders the block layout the statement window uses
  (fact boxes for the updated date, the organization and the standard, the
  contact block first, one `.shayach-block` per section with a line icon).
  It comes with a minimal style, scoped to `.shayach-statement-blocks`, that
  keeps the page's font and colors and adds boxes, borders and spacing. Tune
  it with CSS custom properties set on the container or any ancestor:
  `--shayach-accent` (icons, the contact block's border, the missing-details
  marker; default `currentColor`), `--shayach-surface` (box background;
  default transparent), `--shayach-line` (box borders; default
  `currentColor` at 30%) and `--shayach-muted` (fact labels; default
  inherited). Without `data-layout` the plain markup is unchanged. The
  configurator's statement page code uses `data-layout="blocks"`.

The container attribute's older name, `data-il-a11y-statement`, is still
accepted; use `data-shayach-statement` in new pages.

Notes:

- `window.A11yConfig` must be defined before `statement.js` runs. Like
  `widget.js`, it renders at `DOMContentLoaded` (or immediately if it runs
  later), so an inline config anywhere in the page's HTML works. A config
  defined after the script has run is not picked up.
- If the container sits inside a framework-managed component that re-renders,
  the framework may wipe the rendered statement. Put the container in static
  HTML, or render it after hydration.
- `statement.js` renders only the containers present when it first runs (at
  `DOMContentLoaded`); it does not watch for containers added later. In
  client-routed apps (Next.js, React Router, Vue Router, Nuxt), keep the
  container in the server-rendered HTML of the statement page and make every
  link to that page a full page load (a plain `<a href>`, not the router's
  link component). `statementUrl` in the panel already opens the page in a
  new tab.
- Several containers on one page are all rendered; each is rendered only
  once, even if `statement.js` is included twice, and one broken container
  does not stop the others.
- The output is a `<section class="shayach-statement">` with plain headings,
  paragraphs and lists and no styles of its own, so it takes the page's
  typography. With `data-layout="blocks"` it is a
  `<section class="shayach-statement shayach-statement-blocks">`, and
  `statement.js` adds the block layout's small scoped style sheet to the page
  once, as a constructable stylesheet.
- On page builders whose HTML blocks run in an iframe (for example Wix's
  embed element), the config with `statement` must be inside the same embed as
  the container and `statement.js`, because the iframe has its own `window`.
  Keep the statement details only in that embed (the site-wide config needs
  only `statementUrl`, so the panel's footer and about card link to the page),
  so the two copies cannot drift apart.

### Statement fields

All fields are optional in the schema, but the required items below must be
provided for a complete statement.

| Field | Meaning |
|---|---|
| `organizationName` | Name of the organization. **Required.** |
| `organizationDescription` | One line describing the organization or the site. |
| `commitment` | Your commitment to accessibility, in your own words. |
| `updatedAt` | Date the statement was last reviewed, `YYYY-MM-DD` (a real calendar date). **Required.** |
| `conformanceLevel` | `"A"`, `"AA"` or `"AAA"`. Set it only for a level the site was actually audited at; otherwise the statement states no level. |
| `additionalStandards` | Other standards the site was checked against, for example `["WCAG 2.2 AA"]`. |
| `adjustments` | Accessibility adjustments the site offers. **Required.** |
| `reviewFrequency` | How often accessibility is reviewed, for example "every 6 months". |
| `measures` | Measures taken to keep the site accessible, such as audits or staff training. |
| `browsers` | Browsers the site was tested with. |
| `assistiveTech` | Assistive technologies the site was tested with. |
| `knownLimitations` | Known parts of the site that are not accessible, in plain language ("videos have no captions"), not criterion numbers. |
| `thirdPartyPages` | `[{ url, description }]`: pages whose content comes from third parties. The statement then declares partial conformance for them. `url` is required (`http(s)` or relative). |
| `exemptions` | `[{ scope, type, expires, alternatives }]`: parts of the site for which you rely on a legal exemption (`scope` and `type` are required; `type` is `financial`, `technological` or `other`; a technological exemption must also list `alternatives`, per regulation 35ו(ג)). See [Exemptions](#exemptions). |
| `stage` | `{ description, completionDate, alternatives }`: use when the site is still being made accessible. `description` is required. |
| `temporaryNotices` | `[{ text, until }]`: current notices about temporary accessibility faults. Remove them when fixed. |
| `physicalArrangements` | Accessibility arrangements at the physical premises. **Required** unless `noPhysicalService` is true. |
| `noPhysicalService` | `true` when there is no physical place that serves the public. |
| `coordinator` | `{ name, phone, email, other }`: the accessibility coordinator. |
| `contact` | `{ name, phone, email, other }`: a general accessibility contact. |

All dates (`updatedAt`, `expires`, `completionDate`, `until`) are
`YYYY-MM-DD`.

**Owner text in several languages.** Every text you write (the organization
name and description, `commitment`, `reviewFrequency`,
`physicalArrangements`, each item of `additionalStandards`, `adjustments`,
`measures`, `browsers`, `assistiveTech` and `knownLimitations`, a third-party
page's `description`, an exemption's `scope` and `alternatives`, the stage's
`description` and `alternatives`, a notice's `text`, and a contact's `name`
and `other`) is either one string or an object with one value per language
(`he`, `ar`, `ru`, `en`; at least one):

```js
organizationName: { he: "מאפיית הדוגמה", en: "Example Bakery" },
adjustments: [
  { he: "ניווט מלא במקלדת", en: "Full keyboard navigation" },
  "NVDA",
],
```

The statement shows the value in its own language, else the value in the
page's language (`<html lang>`), else the first value given. A plain string is
shown as written in every language, so write it in the site's language. Text
that is not in the statement's language is marked with its own language (a
plain string counts as the page's language) so screen readers pronounce it
correctly. Phone numbers, emails, URLs and dates are never per-language.

### Required items and how missing ones are handled

The required items (`REQUIRED_ITEMS` in `src/statement/model.ts`) are checked
like this:

| Required item | Satisfied when |
|---|---|
| Organization name | `organizationName` is a non-empty string. |
| Updated date | `updatedAt` is a valid `YYYY-MM-DD` date. |
| Adjustments made | `adjustments` has at least one non-empty string. |
| Contact | a valid `coordinator` or `contact` exists: an object with at least a `phone`, `email` or `other` (a name alone is not enough). |
| Physical arrangements | `physicalArrangements` is set, or `noPhysicalService` is `true`. |

Required details that are missing are listed at the top of the statement and
logged as `[shayach]` console warnings. Nothing is invented.

### Rendering rules

- The statement title, then the updated date, then the "missing details"
  notice (if any), then the organization's introduction and `commitment`.
- Fixed sections from the official template (what an accessible site is, the
  legal standard IS 5568 based on WCAG 2.0) are always shown.
- The adjustments list shows the owner's `adjustments` plus one fixed item
  describing the widget's panel. Owner adjustments should therefore describe
  what was done on the site itself.
- A conformance level appears only when `conformanceLevel` is set.
- Optional sections (temporary notices, measures, known limitations,
  exemptions, third-party pages, tested browsers and assistive technologies,
  accessibility stage, physical arrangements) appear only when their fields
  are set.
- In the statement window (and with `data-layout="blocks"`) the contact section comes first, right after the fact boxes, with its introduction and the demand sentence; it is not repeated at the end.
- Contact: if both `coordinator` and `contact` are set, only `coordinator` is
  shown. A phone number becomes a `tel:` link (plain text if it has no
  digits); an email becomes a `mailto:` link.
- The statement never says the site is fully accessible.
- Limits: each text value is cut to 2,000 characters and each list to 50
  items. Every value is inserted as text, never as HTML.

### Statement warnings

Besides the missing-items warning, these are logged as `[shayach]` warnings
and the value is ignored: an unknown `statement` key, an invalid date, a
`conformanceLevel` other than `A`, `AA` or `AAA`, an invalid email, a phone
number that is not digits with optional `+ ( ) -` and spaces (at least 6
characters), a contact with only a name, a third-party page without a valid
URL, an exemption without a `scope` or with an unknown `type`, and a
technological exemption without `alternatives` (logged; the exemption is
still shown), and in per-language text a key other than `he`, `ar`, `ru` or
`en` or a value that is not a string (that value is ignored, the others are
kept).

## Exemptions

Some small providers may be exempt from the website accessibility
requirements, under certain conditions.
The two official sources differ, and this project does not decide which one
applies to anyone. It never encodes a turnover threshold in its logic and
never decides that anyone is exempt; the statement's exemptions section only
repeats what the owner configures.

- Regulation 35ו(ז), as consolidated on
  [Nevo](https://www.nevo.co.il/law_html/law01/500_865.htm), exempts an עוסק
  פטור or a provider whose average annual turnover does not exceed 100,000 ILS.
- The Commission's guide "נגישות אתרי אינטרנט" (updated 16.03.2026,
  [gov.il](https://www.gov.il/he/pages/website_accessibility?chapterIndex=8))
  gives the threshold as 120,000 ILS.
- Because the two official sources differ, a provider that is not an עוסק פטור
  and has turnover between 100,000 and 120,000 ILS should not assume it is
  exempt, and should check with the Commission or a lawyer.
- A provider with average annual turnover up to 1,000,000 ILS may be entitled
  to a renewable 3-year exemption only for a site first operated before 26
  October 2017,
  conditional on publishing their contact details accessibly. A new site must
  be accessible.
- Per the Commission, these turnover exemptions do not apply to public
  authorities.

This is not legal advice.

## Content Security Policy

If the site sends a CSP header, allow `https://shayach.co.il` in
`script-src`. This also covers `statement.js` on the statement page, and the
statement window, which loads one chunk from the same `/v1/` path only when a visitor opens it and adds its styles (and, while printing, a print-only sheet on the page) as constructable stylesheets. If you want the dyslexia font to work, also allow
`https://shayach.co.il` in `font-src`. That is all: the widget
needs no `'unsafe-inline'` in `style-src`, because it adds its styles as
constructable stylesheets, which a Content Security Policy does not block.
The same goes for the style sheet of the statement page's block layout
(`data-layout="blocks"`).

The one exception is a site that sends its CSP as a header (with no `nonce`
on its scripts or styles and no CSP `<meta>` tag) and either uses CSS cascade
layers (for example Tailwind v4) or loads a stylesheet from another origin
without CORS (for example the Google Fonts CSS), whose rules the widget cannot
read. While an adjustment is active, the widget then tries to add a one-line
`<style>` first in `<head>` that orders its layer before the site's. The
header CSP blocks it, which the browser reports as one violation per page
load, only after a visitor turns on an adjustment. A CSP set in a `<meta>`
tag, or any `nonce` on the site's scripts or styles, is detected and the
`<style>` is skipped, so those sites get no report.

Example additions to an existing policy:

```
script-src 'self' https://shayach.co.il;
font-src 'self' https://shayach.co.il;
```

## What it does

**For visitors**, a floating button opens a simple panel (a side drawer on the
button's side, checked each time the panel opens; a full-screen sheet on
phones) with:

- **Profiles** (one click each):

  | Profile | Turns on |
  |---|---|
  | Seizure safe | stop animations, low saturation |
  | Vision impaired | font size 130%, high contrast, big cursor, highlight links |
  | ADHD friendly | reading mask, stop animations, highlight headings |
  | Cognitive | readable font, highlight links, highlight headings, reading guide |
  | Dyslexia | dyslexia font, letter spacing, line height |
  | Keyboard navigation | emphasize focus, highlight links, highlight headings |

- **Text**: font size (100% to 200% in steps of 10%), line height (1.8),
  letter spacing (with word spacing), readable font, dyslexia font
  (OpenDyslexic, downloaded only when turned on), align text (text blocks
  aligned to the start side), highlight links, highlight headings.
- **Color**: dark, light and high contrast, monochrome, low and high
  saturation.
- **Navigation and display**: stop animations, hide images, emphasize focus,
  big cursor, reading guide, reading mask, and page structure (jump to the
  page's headings and regions).
- **Read aloud** with adjustable reading speed (50% to 200%), using the
  browser's built-in voices: click or focus any text to hear it. Escape stops
  reading.
- **Cards**: each group (profiles, text, color, navigation and display, read aloud) is a collapsible card, one open at a time, with a badge counting the adjustments that are on in it. The open card is remembered for the visit (`sessionStorage` key `shayach:card`).
- **Button position**: drag the button to one of ten spots (five on each side), or pick one in the "Button position" card (a 5 x 2 grid of toggle buttons, also by keyboard, with a button back to the default). Saved in the visitor's browser; Reset settings restores the owner's position. Dragging works while the panel is closed: dashed outlines show the ten spots, the button snaps to the nearest one on release, and Escape cancels the drag. With `icon.draggable: false` there is no dragging and no position card.
- **About the accessibility menu**: what the menu is, the tools this build has, a note that it complements but does not replace an accessible site, and the way to the full statement.
- Language switch, accessibility statement, reset, and hide widget (hides the
  button for the rest of the visit).

Options that cannot be combined are exclusive: one contrast mode at a time,
one of readable font and dyslexia font, one of reading guide and reading mask,
one of monochrome and the saturation modes. Settings are remembered as the
visitor moves between pages. The panel works with a keyboard and screen
readers, and Escape closes it.

**For site owners**, everything visual is configurable: the button's colors,
shape, size, position and label, whether visitors may move it, and the
panel's colors and scrollbar color. The button always shows the universal
accessibility symbol. The panel's buttons
are fixed so every site offers the same complete set. The generated
statement is described in [Accessibility statement](#accessibility-statement).

## What it does not do

Israeli law (Regulation 35 of the 2013 Service Accessibility Regulations)
requires the **site itself** to meet IS 5568 at level AA and to publish an
accessibility statement. A widget cannot fix inaccessible markup. This project
gives the widget and the statement; the site still needs:

- text alternatives for images,
- sufficient color contrast in the design,
- full keyboard operability and visible focus,
- labeled form fields and meaningful headings and landmarks,
- captions for video content.

Known technical limits of the widget:

- **Stop animations** stops CSS animations, transitions and smooth scrolling,
  pauses autoplaying videos (including ones added later) and freezes
  same-origin GIFs. It cannot stop every JavaScript-driven animation.
  Cross-origin GIFs without CORS keep playing, and on sites whose CSP blocks
  `data:` images, frozen GIFs may appear blank.
- **Hide images** keeps images in the layout and available to screen readers.
  It does not hide `<canvas>`, so maps and charts keep working.
- **Page structure** lists the visible headings and landmarks (forms and
  regions only when they have a name) from the page's markup, so it only
  shows what the markup provides.
- **Reading guide and reading mask** follow the mouse and are mouse-only; they
  appear on desktop only. On touch-only devices they are disabled with an
  explanation.
- **Read aloud** reads on click or keyboard focus, using the device's voice for
  each element's language. It depends on the visitor's device: Hebrew and
  Arabic voices are good on iOS, Android and Edge, but missing on some desktop
  browsers. When the page language has no voice, the button is disabled with
  an explanation. Some browsers stop online voices after a short time, so the
  widget prefers voices installed on the device.
- **Dyslexia font** (OpenDyslexic) covers Latin script; for Hebrew, Arabic and
  Russian characters it lacks, the browser falls back to Arial automatically.
- **Dark and light contrast** replace page colors with solid ones and remove
  decorative background images, because text over a background photo cannot be
  made readable otherwise. Images, videos and SVG keep their colors. Icons
  drawn with CSS masks can disappear under dark or light contrast.
- Colors and font sizes that a page writes into an element's own `style`
  attribute with `!important` are overridden. Other properties set that way
  (for example a font family or a background on a link) stay as the page set
  them.
- **Letter spacing** never spaces Arabic letters, but it can only recognize
  Arabic text by its `lang` attribute. Arabic text on a page or element
  without an Arabic `lang` may get letter spacing.
- **Monochrome, saturation and high contrast** apply a filter to the whole
  page, including the accessibility panel itself.
- On sites with a strict Content Security Policy (no `'unsafe-inline'` in
  `style-src`) that also use cascade-layer `!important` utilities (for example
  Tailwind v4 `text-black!`), those utilities can still win over the widget's
  colors and fonts.
- Content inside cross-origin iframes, canvas-rendered text, and closed
  shadow roots cannot be adjusted.
- Content inside open shadow roots (web components) gets the same style
  adjustments, font size and line height as the rest of the page, for up to
  500 shadow roots per page. Not inside them: contrast's override of inline
  `!important` colors, stop animations' pausing of videos and GIFs, and page
  structure. Browsers without constructable stylesheets get no style
  adjustments inside shadow roots. A shadow root that a script adds to an
  element already on the page is found only when that element is a custom
  element being defined. A component whose own styles use cascade-layer
  `!important` rules can still win over the widget's colors and fonts inside it.
  A component that replaces its own adopted stylesheets after the widget found
  it loses the widget's styles until the next change of an adjustment.

## Standards: WCAG 2.2 AA

This project targets **WCAG 2.2 level AA** for its own interface, which goes
beyond what Israeli law currently requires:

- **The law:** the current Israeli standard, IS 5568 part 1 (September 2023),
  is based on **WCAG 2.0** with a few national changes, at level AA. Some
  sources claim it is based on WCAG 2.1 or 2.2; the official text says 2.0.
- **This project:** the widget's own interface meets WCAG 2.2 AA plus IS
  5568's national additions (for example, 2.4.10 Section Headings is required
  at AA). Since WCAG 2.2 builds on 2.0, meeting it also covers the legal
  baseline for the widget's own interface. It says nothing about the rest of
  the site.
- **The site:** we recommend building to WCAG 2.2 AA too. The accessibility
  statement states the standard and level the owner configures, never a level
  they have not claimed.

## Privacy, footprint and performance

- **Privacy:** no cookies, no analytics, no tracking and no account. The
  widget sends no visitor data anywhere; the only network requests are for
  its own files from the `/v1/` path. The visitor's adjustments,
  language and chosen button position are stored only in the visitor's own
  browser (`localStorage` key `shayach:v1`, written only after they
  change something), and "hide widget" uses `sessionStorage` (key
  `shayach:hidden`) for the current visit. The open panel card is kept in
  `sessionStorage` (key `shayach:card`) for the visit.
  Read aloud uses the browser's own speech engine.
- **Footprint:** `widget.js` is under 7 KB gzip (a size budget enforced in the
  test suite) and does not block rendering. Dragging adds no page listeners
  while idle: one `keydown` listener exists only during a drag. The panel code (`app.js`, under
  30 KB gzip) loads only when a visitor opens the panel or has saved
  adjustments. The widget renders in Shadow DOM, adds no global besides the
  site's own `window.A11yConfig`, and removes everything it added when an
  adjustment is turned off or reset.
- **Performance:** measured in Chromium on a desktop computer, on a test page
  with 5,000 paragraphs: enlarging text applies in about 40 ms and dark
  contrast in about 16 ms, and loading the widget causes no task over 50 ms.
  When that page adds 50 paragraphs every 100 ms while text is enlarged, the
  widget adds about 8 ms of work per batch to scale them. Slower devices take
  longer.

## Versioning and updates

- Sites load the widget from a **major-version URL** (`/v1/`). Every `1.x.y`
  release, including legal updates, reaches all sites automatically within
  minutes (files under `/v1/` are cached for 5 minutes).
- `1.x` never contains a breaking change. Renaming or removing a config field,
  or changing the script URL, only ever ships as a new major version (`/v2/`),
  which a site adopts by changing its script URL. One exception: `icon.image`
  was removed in 1.6.0, before any site had installed the widget with it (see
  the changelog); a config that still sets it gets a warning and the regular icon.
- Every release is listed in [CHANGELOG.md](../CHANGELOG.md) (Keep a
  Changelog 1.1.0, Semantic Versioning). Entries marked
  **`[Action required]`** tell site owners exactly what to change; entries
  marked **`[Legal]`** come from a change in Israeli law or standards and
  link the official source. Entries without `[Action required]` need nothing
  from the site.
- Machine-readable changelog:
  `https://shayach.co.il/v1/changelog.json`, generated from
  `CHANGELOG.md` at build time: `{ "versions": [...] }`, newest first
  (starting with `Unreleased`), each with `version`, `date` and `sections`
  (such as `Added`, `Changed`, `Fixed`), each entry with `text`, `legal`,
  `actionRequired` and `links`. A withdrawn version has `yanked: true`.
- Deployed build: `https://shayach.co.il/v1/build.json` gives
  the deployed `version` and `commit`.
- A summary for AI agents is published at
  <https://shayach.co.il/llms.txt>.

## License

[Apache License 2.0](../LICENSE). Anyone may use, fork and modify this
project, including commercially. Redistributions and derivative works must
keep the attribution in [NOTICE](../NOTICE).
