From aea52b9b73ed0b445fd5ed4847b988752932d395 Mon Sep 17 00:00:00 2001 From: Jacques Newman Date: Wed, 10 Jun 2026 15:58:09 -0700 Subject: [PATCH 01/22] Add aria-actions interactive demo Single-page demo illustrating the proposed `aria-actions` ARIA attribute (w3c/aria PR 1805) across three composite-widget patterns: tabs with per-tab action buttons, listbox with per-option actions, and a data grid with row-level actions. Requires the `AriaActions` Blink runtime feature flag (`chrome://flags/#enable-experimental-web-platform-features` or `--enable-blink-features=AriaActions`). The root README index row for the new demo is intentionally left for a follow-up commit. --- README.md | 1 + aria-actions/.gitignore | 1 + aria-actions/README.md | 25 ++ aria-actions/index.html | 623 ++++++++++++++++++++++++++++++++++++++++ aria-actions/style.css | 519 +++++++++++++++++++++++++++++++++ 5 files changed, 1169 insertions(+) create mode 100644 aria-actions/.gitignore create mode 100644 aria-actions/README.md create mode 100644 aria-actions/index.html create mode 100644 aria-actions/style.css diff --git a/README.md b/README.md index d90c4231..9e94792d 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,7 @@ See: | Demo name | Description and docs | Source code & Readme | Live demo page | |---|---|---|---| +| ARIA Actions demo | Interactive demo for the `aria-actions` ARIA attribute, which exposes a list of secondary actions on composite-widget items (tabs, options, rows) to assistive technology. | [/aria-actions/](https://github.com/MicrosoftEdge/Demos/tree/main/aria-actions) | [ARIA Actions demo](https://microsoftedge.github.io/Demos/aria-actions/) | | Built-in AI playgrounds | Demo pages showing how to use the built-in Prompt and Writing Assistance AI APIs in Microsoft Edge. | [/built-in-ai/](https://github.com/MicrosoftEdge/Demos/tree/main/built-in-ai) | [Built-in AI playgrounds](https://microsoftedge.github.io/Demos/built-in-ai/) demo | | CSS Gap Decorations demos | Draws line decorations within gaps in CSS Grid, Flexbox, and Multi-column layouts. | [/css-gap-decorations/](https://github.com/MicrosoftEdge/Demos/tree/main/css-gap-decorations) | [CSS Gap Decorations demos](https://microsoftedge.github.io/Demos/css-gap-decorations/) (Readme) | | CSS Masonry demos | Showcase the implementation of CSS Masonry layout. | [/css-masonry/](https://github.com/MicrosoftEdge/Demos/tree/main/css-masonry) | [CSS Masonry demos](https://microsoftedge.github.io/Demos/css-masonry/) (Readme) | diff --git a/aria-actions/.gitignore b/aria-actions/.gitignore new file mode 100644 index 00000000..32bd4e63 --- /dev/null +++ b/aria-actions/.gitignore @@ -0,0 +1 @@ +.screenshots/ diff --git a/aria-actions/README.md b/aria-actions/README.md new file mode 100644 index 00000000..c592b7c9 --- /dev/null +++ b/aria-actions/README.md @@ -0,0 +1,25 @@ +# ARIA actions demo + +➡️ **[Open the demo](https://microsoftedge.github.io/Demos/aria-actions/)** ⬅️ + +Interactive demo of [`aria-actions`](https://pr-preview.s3.amazonaws.com/w3c/aria/pull/1805.html), a proposed ARIA attribute that exposes secondary actions on an element to assistive technology. The value is an IDREFS list; each referenced element becomes an action that screen-reader users can invoke directly from the focused item. + +The demo covers three composite-widget patterns: tabs, a listbox, and a data grid. Naming and structure follow [ARIA PR 1805](https://github.com/w3c/aria/pull/1805) and the [APG Tabs with Action Buttons example](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-actions/). + +## Learn more + +- [ARIA spec PR 1805 — rendered preview](https://pr-preview.s3.amazonaws.com/w3c/aria/pull/1805.html) (current normative text) +- [Umbrella discussion: "Secondary actions on items in composite widget roles"](https://github.com/w3c/aria/issues/1440) +- [APG: Tabs with Action Buttons example](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-actions/) +- [Proposal explainer (Sarah Higley)](https://gist.github.com/smhigley/8dbe67f834cc472e3a14bf6b289e6f0c) + +## Requirements + +The demo exercises Blink's **AriaActions** runtime feature, which is experimental and off by default in every channel. To turn it on: + +| Method | Instructions | +|--------|--------------| +| `chrome://flags` | Open `chrome://flags/#enable-experimental-web-platform-features`, set it to **Enabled**, then relaunch. | +| Command line | Launch with `--enable-blink-features=AriaActions`. | + +Microsoft Edge or any Chromium-based browser will work. Firefox and Safari have not shipped support. diff --git a/aria-actions/index.html b/aria-actions/index.html new file mode 100644 index 00000000..3f195447 --- /dev/null +++ b/aria-actions/index.html @@ -0,0 +1,623 @@ + + + + + + + ARIA Actions: Interactive Demo + + + + + + + + + +
+ + + + + + + + +
+

What is aria-actions?

+

+ aria-actions is a proposed ARIA attribute. Authors point + it at a list of element IDs; each referenced element becomes a + secondary action that screen-reader users can invoke from the focused + item, without first moving focus to the action button. +

+

+ The value is an IDREFS list (space-separated IDs). Each target needs + an accessible name, a click handler, and a keyboard path. See the + explainer + for the full authoring requirements. +

+

+ Spec status: + w3c/aria PR 1805, + still under review. The + rendered + preview has the current normative text, and the + APG + Tabs with Action Buttons example shows a working reference + implementation. +

+
+ + + + +
+

Tabs with Actions

+

+ Each tab points at two action buttons. A screen reader can invoke + Close or Pin from the tab itself; keyboard users press + Delete on the focused tab as a shortcut for Close. +

+
aria-actions="close-tab-N pin-tab-N"
+ +
+ Try it: Tab into the tablist; + //Home/End + move focus, and Delete closes the focused tab. With a + screen reader, Close and Pin appear as actions on whichever tab has + focus. +
+ +
+
+ + + +
+ +
+

Contents of index.html.

+
+ + + + +
+ + + + + + +
+
+ +
    +
  • Each tab's aria-actions lists two button IDs
  • +
  • The buttons sit outside the tablist. role="tab" is + Children Presentational, so any interactive element nested inside a + tab loses its semantics; siblings are the safe place for them
  • +
  • Each button also sets aria-controls back to its tab. + That fits here because Close and Pin really do control the tab + (the listbox and grid sections omit the back-reference for that + reason)
  • +
  • Delete on the focused tab triggers Close; + aria-keyshortcuts tells AT about the shortcut
  • +
+
+ + + + +
+

Listbox with Actions

+

+ Each option points at two per-item buttons. AT users invoke Edit or + Delete from the option without moving off it. The same buttons live + in a side panel next to the listbox, so non-AT keyboard users can Tab + over and press them too. +

+
aria-actions="edit-item-N delete-item-N"
+ +
+ Try it: Tab into the listbox; + //Home/End + change the selection. Tab again to land on the side panel and press + Edit or Delete by keyboard. Esc from a button returns + focus to the listbox. With a screen reader, both actions are listed + on whichever option has focus. +
+ +
+
+
    +
  • MDN Web Docs
  • +
  • CSS Tricks
  • +
  • Web.dev
  • +
+ + +
+ + + + + + + + + + + + +
+
+
+ +
    +
  • Each role="option" lists its action button IDs in + aria-actions
  • +
  • The buttons cannot live inside the <li>: + role="option" is Children Presentational in ARIA + 1.2/1.3, so any nested interactive element loses its semantics. + They live in a side panel instead
  • +
  • The JS keeps tabindex and aria-selected + in sync as //Home/End + move the selection
  • +
  • Tab reaches the side panel; Esc sends focus back to + the listbox
  • +
  • The side panel uses role="group" so its + aria-label reaches AT. A bare <div> + would swallow the label
  • +
+
+ + + + +
+

Grid with Row Actions

+

+ Each row exposes its action buttons via aria-actions set + on the <tr>. AT users invoke View details or + Archive from the row, without descending into the Actions cell. +

+
aria-actions="view-row-N archive-row-N"
+ +
+ Try it: Tab into the grid; + //Home/End + move between rows. Tab from a row to reach its action buttons, and + Esc to return. With a screen reader, View and Archive + appear as actions on whichever row has focus. +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OrderCustomerStatusActions
#1024Alice JohnsonShipped + + + + +
#1025Bob ChenProcessing + + + + +
#1026Carol DavisDelivered + + + + +
+
+ +
    +
  • aria-actions on each role="row" exposes + the row's actions
  • +
  • The buttons live in the Actions column. Only the focused row's + buttons are Tab stops; the JS swaps tabindex as row + focus moves
  • +
  • Esc from a button returns focus to its row + (APG grid widget-mode)
  • +
  • The exact set of roles that can carry aria-actions + is still under discussion in PR 1805. + So far the consensus targets child roles of composite widgets: + tabs, options, rows, gridcells, treeitems, and similar
  • +
  • Roving tabindex on <tr> plus the + arrow-key handler make the grid keyboard-operable
  • +
+
+ +
+ + + + + diff --git a/aria-actions/style.css b/aria-actions/style.css new file mode 100644 index 00000000..e1440086 --- /dev/null +++ b/aria-actions/style.css @@ -0,0 +1,519 @@ +/* ============================================================ + ARIA Actions Demo - Stylesheet + Adapted from the focusgroup demo's visual conventions. + ============================================================ */ + +/* ----- Reset & Base ----- */ +* { + box-sizing: border-box; +} + +html, +input, +textarea, +button { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + -webkit-text-size-adjust: 100%; + -moz-text-size-adjust: 100%; + text-size-adjust: 100%; + font-size: 16px; + font-family: Segoe UI, SegoeUI, Helvetica Neue, Helvetica, Arial, sans-serif; + font-weight: 400; +} + +html { + min-height: 100vh; +} + +/* ----- CSS Custom Properties (Light theme) ----- + --focus-color is set for >=3:1 contrast against --bg/--bg-card + (WCAG 1.4.11 Non-text Contrast). + #b45309 on #f9fafb >= 4.1:1. */ +:root { + color-scheme: light dark; + --bg: #f9fafb; + --bg-card: #ffffff; + --text: #1f2937; + --text-muted: #6b7280; + --border: #e5e7eb; + --accent: #2563eb; + --accent-light: #dbeafe; + --focus-color: #b45309; + --success: #10b981; + --warning-bg: #fef3c7; + --warning-border: #f59e0b; + --warning-text: #92400e; + --code-bg: #f3f4f6; + --selected-bg: #2563eb; + --selected-text: #ffffff; + --hover-bg: #f3f4f6; + --shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); + --radius: 8px; +} + +/* ----- Dark theme ----- */ +@media (prefers-color-scheme: dark) { + :root { + --bg: #111827; + --bg-card: #1f2937; + --text: #f3f4f6; + --text-muted: #9ca3af; + --border: #374151; + --accent: #60a5fa; + --accent-light: #1e3a5f; + --focus-color: #fbbf24; + --success: #34d399; + --warning-bg: #422006; + --warning-border: #d97706; + --warning-text: #fde68a; + --code-bg: #374151; + --selected-bg: #3b82f6; + --selected-text: #ffffff; + --hover-bg: #374151; + --shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + } +} + +/* ----- Reduced motion ----- */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + transition-duration: 0ms !important; + animation-duration: 0ms !important; + animation-iteration-count: 1 !important; + } +} + +/* ----- Body ----- */ +body { + margin: 0; + padding: 0; + background: var(--bg); + color: var(--text); + line-height: 1.6; +} + +/* ----- Layout ----- */ +.page-header { + background: var(--bg-card); + border-bottom: 1px solid var(--border); + padding: 1.5rem 2rem; + box-shadow: var(--shadow); +} + +.page-header h1 { + margin: 0 0 0.25rem; + font-size: 1.75rem; + font-weight: 700; +} + +.page-header p { + margin: 0; + color: var(--text-muted); +} + +.page-content { + max-width: 960px; + margin: 0 auto; + padding: 2rem; +} + +/* ----- In-page Navigation ----- */ +.page-nav { + display: flex; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 2rem; +} + +.page-nav a { + color: var(--accent); + text-decoration: none; + font-size: 0.9rem; +} + +.page-nav a:hover { + text-decoration: underline; +} + +/* ----- Warning / Feature-detection Banner ----- */ +.warning-banner { + position: relative; + background: var(--warning-bg); + border: 1px solid var(--warning-border); + color: var(--warning-text); + padding: 1rem 2.75rem 1rem 1.25rem; + border-radius: var(--radius); + margin-bottom: 1.5rem; + font-size: 0.95rem; + line-height: 1.5; +} + +.warning-banner strong { + display: block; + margin-bottom: 0.25rem; +} + +.banner-dismiss { + position: absolute; + top: 0.5rem; + right: 0.5rem; + width: 1.75rem; + height: 1.75rem; + padding: 0; + border: 1px solid transparent; + border-radius: 4px; + background: transparent; + color: inherit; + font-size: 1.25rem; + line-height: 1; + cursor: pointer; +} + +.banner-dismiss:hover { + background: rgba(0, 0, 0, 0.08); +} + +/* ----- Demo Section ----- */ +.demo-section { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.5rem; + margin-bottom: 2rem; + box-shadow: var(--shadow); +} + +.demo-section h2 { + margin: 0 0 0.25rem; + font-size: 1.35rem; + font-weight: 600; +} + +.demo-section h3 { + margin: 1.25rem 0 0.5rem; + font-size: 1.1rem; + font-weight: 600; +} + +.demo-section > p { + color: var(--text-muted); + margin: 0.25rem 0 1rem; + font-size: 0.95rem; +} + +/* ----- Demo Container (the interactive area) ----- */ +.demo-container { + border: 2px dashed var(--border); + border-radius: var(--radius); + padding: 1.25rem; + margin: 1rem 0; + background: var(--bg); +} + +/* ----- Code Label ----- +
 retains the literal-text semantics of the displayed attribute
+   while the visual presentation matches inline code styling. */
+.attr-label {
+  display: inline-block;
+  background: var(--code-bg);
+  border: 1px solid var(--border);
+  border-radius: 4px;
+  padding: 0.15rem 0.5rem;
+  margin: 0 0 0.75rem;
+  font-family: Consolas, Monaco, "Courier New", monospace;
+  font-size: 0.85rem;
+  color: var(--accent);
+  white-space: pre;
+}
+
+.attr-label code {
+  background: transparent;
+  padding: 0;
+  font-size: inherit;
+  color: inherit;
+}
+
+/* ----- Try-it Box ----- */
+.try-it {
+  background: var(--accent-light);
+  border-radius: 4px;
+  padding: 0.5rem 0.75rem;
+  margin-bottom: 1rem;
+  font-size: 0.9rem;
+}
+
+.try-it kbd,
+.demo-section kbd {
+  display: inline-block;
+  background: var(--bg-card);
+  border: 1px solid var(--border);
+  border-radius: 3px;
+  padding: 0.1rem 0.4rem;
+  font-family: Consolas, Monaco, "Courier New", monospace;
+  font-size: 0.85rem;
+  box-shadow: 0 1px 0 var(--border);
+}
+
+/* ----- Notice List ----- */
+.notice-list {
+  padding-left: 1.25rem;
+  font-size: 0.9rem;
+  color: var(--text-muted);
+}
+
+.notice-list li {
+  margin-bottom: 0.3rem;
+}
+
+/* ----- Focus Ring ----- */
+:focus-visible {
+  outline: 2px solid var(--focus-color);
+  outline-offset: 2px;
+}
+
+/* ----- Buttons ----- */
+.demo-container button {
+  padding: 0.5rem 1rem;
+  border: 1px solid var(--border);
+  border-radius: 4px;
+  background: var(--bg-card);
+  color: var(--text);
+  cursor: pointer;
+  font-size: 0.9rem;
+  transition: background 0.15s;
+}
+
+.demo-container button:hover {
+  background: var(--hover-bg);
+}
+
+/* ----- Tab Control ----- */
+.tablist {
+  display: flex;
+  gap: 0;
+  border-bottom: 2px solid var(--border);
+}
+
+.tablist .tab {
+  border: none;
+  border-radius: 0;
+  background: transparent;
+  padding: 0.65rem 1.25rem;
+  font-weight: 500;
+  color: var(--text-muted);
+  position: relative;
+  cursor: pointer;
+}
+
+/* Selected tab uses font-weight + color + bottom border so the state
+   is conveyed by more than color alone (WCAG 1.4.1). */
+.tablist .tab[aria-selected="true"] {
+  color: var(--accent);
+  font-weight: 700;
+  border-bottom: 2px solid var(--accent);
+  margin-bottom: -2px;
+}
+
+.tabpanel {
+  padding: 1rem;
+  border: 1px solid var(--border);
+  border-top: none;
+  border-radius: 0 0 var(--radius) var(--radius);
+  min-height: 60px;
+}
+
+.tab-actions {
+  display: inline-flex;
+  gap: 0.25rem;
+  margin-left: 0.5rem;
+  margin-top: 0.75rem;
+  vertical-align: middle;
+}
+
+.tab-actions button {
+  padding: 0.2rem 0.5rem;
+  font-size: 0.8rem;
+  border: 1px solid var(--border);
+  border-radius: 3px;
+  background: var(--bg-card);
+  color: var(--text);
+  cursor: pointer;
+}
+
+.tab-actions button:hover {
+  background: var(--hover-bg);
+}
+
+/* ----- Listbox-with-actions layout -----
+   Listbox in the left column; action buttons in a sibling panel on the
+   right. Each action row visually aligns with its option via matching
+   row heights. Buttons are NOT inside the 
  • elements + (option has Children Presentational: True; nested interactives would + lose their semantics). */ +.listbox-with-actions { + display: grid; + grid-template-columns: 1fr auto; + border: 1px solid var(--border); + border-radius: var(--radius); + max-width: 500px; + background: var(--bg-card); + overflow: hidden; +} + +.listbox-with-actions .listbox { + border: none; + border-radius: 0; + max-width: none; + margin: 0; +} + +.listbox { + list-style: none; + margin: 0; + padding: 0; + border: 1px solid var(--border); + border-radius: var(--radius); + max-width: 400px; +} + +.listbox-option { + padding: 0.6rem 1rem; + border-bottom: 1px solid var(--border); + cursor: default; +} + +.listbox-option:last-child { + border-bottom: none; +} + +.listbox-option:hover { + background: var(--hover-bg); +} + +.listbox-option[aria-selected="true"] { + background: var(--accent-light); +} + +.bookmark-actions { + display: grid; + grid-template-rows: repeat(3, 1fr); + border-left: 1px solid var(--border); +} + +.bookmark-actions .action-row { + display: flex; + gap: 0.25rem; + align-items: center; + padding: 0.6rem 1rem; + border-bottom: 1px solid var(--border); +} + +.bookmark-actions .action-row:last-child { + border-bottom: none; +} + +.bookmark-actions button { + padding: 0.2rem 0.5rem; + font-size: 0.8rem; + border: 1px solid var(--border); + border-radius: 3px; + background: var(--bg-card); + color: var(--text); + cursor: pointer; +} + +.bookmark-actions button:hover { + background: var(--hover-bg); +} + +/* ----- Grid ----- */ +.data-grid { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} + +.data-grid th, +.data-grid td { + border: 1px solid var(--border); + padding: 0.6rem 0.75rem; + text-align: left; +} + +.data-grid th { + background: var(--code-bg); + font-weight: 600; +} + +.data-grid tbody tr:hover td { + background: var(--hover-bg); +} + +/* Row focus is the primary unit for grid keyboard nav; show a visible + indicator on the focused row. */ +.data-grid tbody tr:focus-visible { + outline: 2px solid var(--focus-color); + outline-offset: -2px; +} + +.row-actions { + display: flex; + gap: 0.25rem; +} + +.row-actions button { + padding: 0.2rem 0.5rem; + font-size: 0.8rem; + border: 1px solid var(--border); + border-radius: 3px; + background: var(--bg-card); + color: var(--text); + cursor: pointer; +} + +.row-actions button:hover { + background: var(--hover-bg); +} + +/* ----- Inline Code ----- */ +code { + font-family: Consolas, Monaco, "Courier New", monospace; + font-size: 0.9em; + background: var(--code-bg); + padding: 0.1rem 0.3rem; + border-radius: 3px; +} + +/* ----- Links ----- */ +a { + color: var(--accent); +} + +/* ----- Forced Colors Mode (Windows High Contrast) ----- + Ensure selected states and focus rings are conveyed via system + colors (which override CSS colors in HCM). */ +@media (forced-colors: active) { + :focus-visible { + outline: 2px solid Highlight; + outline-offset: 2px; + } + + .tablist .tab[aria-selected="true"] { + border-bottom-color: Highlight; + color: Highlight; + } + + .listbox-option[aria-selected="true"] { + background: Highlight; + color: HighlightText; + forced-color-adjust: none; + } + + .data-grid tbody tr:focus-visible { + outline: 2px solid Highlight; + } +} From 9b189a2885f9c8b1a33fdfd4787310e9ad4aa245 Mon Sep 17 00:00:00 2001 From: Jacques Newman Date: Wed, 17 Jun 2026 13:16:32 -0700 Subject: [PATCH 02/22] [aria-actions] Remove window.alert, reframe spec prose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace window.alert() in the delegated data-action click handler with a polite live region (#action-toast, role=status aria-live=polite aria-atomic=true). Toast is always in the DOM, visible only when non-empty, auto-clears after 4s, and honours prefers-reduced-motion and forced-colors. No focus is moved. - Reframe forward-looking prose now that the attribute is shipping behind a flag: "a proposed ARIA attribute" -> "a new ARIA attribute" (index.html:57, README.md:5); "Spec status … still under review" -> "Reference … defines the attribute" (index.html:69-77); "still under discussion in PR 1805 … the consensus targets …" -> "defined by PR 1805 and targets …" (index.html:352-355). Runtime-flag and Firefox/Safari-haven't-shipped wording in the README is left untouched because it's still accurate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- aria-actions/README.md | 2 +- aria-actions/index.html | 41 ++++++++++++++++++++++++++++++---------- aria-actions/style.css | 42 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/aria-actions/README.md b/aria-actions/README.md index c592b7c9..d4080251 100644 --- a/aria-actions/README.md +++ b/aria-actions/README.md @@ -2,7 +2,7 @@ ➡️ **[Open the demo](https://microsoftedge.github.io/Demos/aria-actions/)** ⬅️ -Interactive demo of [`aria-actions`](https://pr-preview.s3.amazonaws.com/w3c/aria/pull/1805.html), a proposed ARIA attribute that exposes secondary actions on an element to assistive technology. The value is an IDREFS list; each referenced element becomes an action that screen-reader users can invoke directly from the focused item. +Interactive demo of [`aria-actions`](https://pr-preview.s3.amazonaws.com/w3c/aria/pull/1805.html), a new ARIA attribute that exposes secondary actions on an element to assistive technology. The value is an IDREFS list; each referenced element becomes an action that screen-reader users can invoke directly from the focused item. The demo covers three composite-widget patterns: tabs, a listbox, and a data grid. Naming and structure follow [ARIA PR 1805](https://github.com/w3c/aria/pull/1805) and the [APG Tabs with Action Buttons example](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-actions/). diff --git a/aria-actions/index.html b/aria-actions/index.html index 3f195447..d0d43f0e 100644 --- a/aria-actions/index.html +++ b/aria-actions/index.html @@ -54,7 +54,7 @@

    ARIA Actions Demo

    What is aria-actions?

    - aria-actions is a proposed ARIA attribute. Authors point + aria-actions is a new ARIA attribute. Authors point it at a list of element IDs; each referenced element becomes a secondary action that screen-reader users can invoke from the focused item, without first moving focus to the action button. @@ -66,9 +66,9 @@

    What is aria-actions?

    for the full authoring requirements.

    - Spec status: - w3c/aria PR 1805, - still under review. The + Reference: + w3c/aria PR 1805 + defines the attribute. The rendered preview has the current normative text, and the APG @@ -349,10 +349,10 @@

    Grid with Row Actions

    focus moves
  • Esc from a button returns focus to its row (APG grid widget-mode)
  • -
  • The exact set of roles that can carry aria-actions - is still under discussion in PR 1805. - So far the consensus targets child roles of composite widgets: - tabs, options, rows, gridcells, treeitems, and similar
  • +
  • The set of roles that can carry aria-actions + is defined by PR 1805 + and targets child roles of composite widgets: tabs, options, rows, + gridcells, treeitems, and similar
  • Roving tabindex on <tr> plus the arrow-key handler make the grid keyboard-operable
  • @@ -360,6 +360,13 @@

    Grid with Row Actions

    + +
    + diff --git a/aria-actions/style.css b/aria-actions/style.css index e1440086..56b28920 100644 --- a/aria-actions/style.css +++ b/aria-actions/style.css @@ -173,6 +173,48 @@ body { cursor: pointer; } +/* ----- Action announcement toast ----- + Polite live region; visible only when non-empty so it doesn't + clutter the page or trap focus. Replaces window.alert() in the + delegated action-button click handler. */ +.action-toast { + position: fixed; + bottom: 1.5rem; + left: 50%; + transform: translateX(-50%); + max-width: calc(100vw - 2rem); + padding: 0.625rem 1.25rem; + background: var(--accent); + color: var(--selected-text); + border: 1px solid var(--accent); + border-radius: var(--radius); + box-shadow: var(--shadow); + font-size: 0.95rem; + line-height: 1.4; + z-index: 1000; + opacity: 0; + transition: opacity 0.18s ease; + pointer-events: none; +} + +.action-toast:not(:empty) { + opacity: 1; +} + +@media (prefers-reduced-motion: reduce) { + .action-toast { + transition: none; + } +} + +@media (forced-colors: active) { + .action-toast { + background: Canvas; + color: CanvasText; + border-color: CanvasText; + } +} + .banner-dismiss:hover { background: rgba(0, 0, 0, 0.08); } From 3f233f4e5d94a1cb6f38a897c406e4b2c9ac5047 Mon Sep 17 00:00:00 2001 From: Jacques Newman Date: Thu, 18 Jun 2026 14:52:45 -0700 Subject: [PATCH 03/22] [aria-actions] Fix WCAG AA contrast + repeat live-region announce MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verification pass on top of 9b189a2 using axe-core 4.11.4 against http://localhost:8765/. Fixes the real findings; the only remaining axe violation is `aria-valid-attr` on the new `aria-actions` attribute itself, which axe-core does not yet recognize (expected — the attribute is the subject of the demo). - code: set explicit color: var(--text). Inline inside .notice-list and .demo-section > p was inheriting --text-muted, which on --code-bg (#6b7280 on #f3f4f6) is ~4.26:1 — under the WCAG 1.4.3 AA 4.5:1 threshold. Switching to --text yields >12:1 in both themes. Resolves all 23 color-contrast occurrences axe-core reported. - .action-toast: switch background from --accent (#60a5fa in dark mode) to --text and color to --bg-card. The previous pairing was ~2.52:1 in dark mode (axe runs in light by default and missed this; computed manually). The inverted-surface pairing is ~14:1 light / ~13:1 dark. - announceAction(): clear textContent before setting, with a 30ms setTimeout, so a click on the same action button twice in a row re-announces. Several screen readers ignore live-region updates whose textContent is unchanged from the prior value. Manual checks (also clean): - Heading order h1 -> h2s preserved; no new headings. - Toast has no focusable children, pointer-events: none — focus is never moved into it. - forced-colors: active still maps toast surfaces to system colors. - prefers-reduced-motion: reduce already disables the fade via the global * transition-duration override; the toast-scoped rule remains as belt-and-suspenders. Not run on this machine: VoiceOver/NVDA SR smoke. Axe-core covers ~20-50% of WCAG; manual SR + keyboard pass before pushing is still recommended (per the original task spec). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- aria-actions/index.html | 10 +++++++++- aria-actions/style.css | 16 +++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/aria-actions/index.html b/aria-actions/index.html index d0d43f0e..68ebe655 100644 --- a/aria-actions/index.html +++ b/aria-actions/index.html @@ -621,9 +621,17 @@

    Grid with Row Actions

    ================================================================ */ var actionToast = document.getElementById("action-toast"); var actionToastTimer = null; + var actionToastSetTimer = null; function announceAction(message) { if (!actionToast) return; - actionToast.textContent = message; + // Clear first so identical successive messages still announce — + // many ATs ignore live-region updates that don't change textContent. + actionToast.textContent = ""; + if (actionToastSetTimer) clearTimeout(actionToastSetTimer); + actionToastSetTimer = setTimeout(function () { + actionToast.textContent = message; + actionToastSetTimer = null; + }, 30); if (actionToastTimer) clearTimeout(actionToastTimer); actionToastTimer = setTimeout(function () { actionToast.textContent = ""; diff --git a/aria-actions/style.css b/aria-actions/style.css index 56b28920..c87b754b 100644 --- a/aria-actions/style.css +++ b/aria-actions/style.css @@ -176,7 +176,9 @@ body { /* ----- Action announcement toast ----- Polite live region; visible only when non-empty so it doesn't clutter the page or trap focus. Replaces window.alert() in the - delegated action-button click handler. */ + delegated action-button click handler. + Uses an inverted-surface pairing (text-tone on card-tone) so contrast + is well above WCAG AA in both themes (~14:1 light, ~13:1 dark). */ .action-toast { position: fixed; bottom: 1.5rem; @@ -184,9 +186,9 @@ body { transform: translateX(-50%); max-width: calc(100vw - 2rem); padding: 0.625rem 1.25rem; - background: var(--accent); - color: var(--selected-text); - border: 1px solid var(--accent); + background: var(--text); + color: var(--bg-card); + border: 1px solid var(--text); border-radius: var(--radius); box-shadow: var(--shadow); font-size: 0.95rem; @@ -521,10 +523,14 @@ body { background: var(--hover-bg); } -/* ----- Inline Code ----- */ +/* ----- Inline Code ----- + Sets an explicit color rather than inheriting from parents like + .notice-list (which uses --text-muted) — that combination dropped + below WCAG AA 4.5:1 against --code-bg. */ code { font-family: Consolas, Monaco, "Courier New", monospace; font-size: 0.9em; + color: var(--text); background: var(--code-bg); padding: 0.1rem 0.3rem; border-radius: 3px; From c85bdae2069a2d87e0fe3bf2e3d1e91b6e4e90c1 Mon Sep 17 00:00:00 2001 From: Jacques Newman Date: Thu, 18 Jun 2026 16:14:12 -0700 Subject: [PATCH 04/22] [aria-actions] Replace toast with real per-item state changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original demo only "fake-fired" actions (alert -> toast). That trains the wrong mental model: aria-actions exists so AT users can invoke things that *do something* to the focused item, mirroring real-world patterns like VS Code tabs and Teams participants (see Sarah Higley's explainer gist, and the APG tabs-actions canonical example which has no toast at all because the action's effect IS the feedback). This change deletes the floating #action-toast and rewires each action button to toggle a real, reversible visible state on its target item. Per-section polite live regions announce the change, and per-section Reset buttons restore starting state so the demo stays replayable. Visible effects: - Tabs: Close strikes through the tab and appends " (closed)"; Pin prefixes the tab name with 📌. - Listbox: Edit italicises and appends " (edited)"; Delete strikes through and appends " (deleted)". - Grid: View highlights the row (--accent-light bg); Archive dims the row and rewrites the Status cell text to "Archived" (the original status is cached on a data-* attr so toggle/Reset can restore it — CSS-only swap wouldn't reach the a11y tree). - All effects toggle on repeat-click; Reset clears everything. Live-region strategy: - Each .demo-section has its own .action-status (role=status, aria-live =polite, aria-atomic=true) co-located with the widget, so AT users hear the announce in context rather than from a global overlay, and sighted users see "Last action: …" where they just clicked. - announceInto() clears msgEl.textContent then sets after 30ms so repeat-click on the same action re-fires the announce (ATs commonly drop unchanged textContent updates). - Reset routes through the same announce path with "Section reset". a11y suffixes (" (closed)", " (edited)", " (deleted)") and the Pin 📌 prefix use CSS ::before/::after content — included in the computed accessible name in all major engines (Chromium / WebKit / Gecko per accname-1.2 §2.F.iii), so AT users navigating to an item after an action was invoked still discover its state, not just at click-time. Suffix colour is var(--text) (not --text-muted) to keep WCAG 1.4.3 AA contrast across both themes against every container background the items can sit in. Visual treatment in forced-colors: - closed tab / deleted option fall back to GrayText (line-through survives HCM as a text-decoration property). - viewing row uses Highlight/HighlightText with forced-color-adjust: none so the highlight remains visible. - archived row resets opacity (HCM ignores it anyway) and switches to GrayText. Verification on this branch: - All 23 puppeteer-driven functional checks pass (toggle state, status text per section, status-cell text swap on archive, Reset clears state + announces "Section reset", repeat-click re-announces). - axe-core 4.11.4 (wcag2a/aa, wcag21a/aa, best-practice) reports only the 9 expected aria-valid-attr false positives on the new `aria-actions` attribute (axe doesn't yet know about it; that's the whole subject of the demo). 0 color-contrast, 0 other violations. - Visual screenshots in both prefers-color-scheme: light and dark render the state cues correctly. README updated with a "What the action buttons do" paragraph so reviewers know what to expect when they click around. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- aria-actions/README.md | 4 + aria-actions/index.html | 188 ++++++++++++++++++++++++++++------------ aria-actions/style.css | 158 ++++++++++++++++++++++++++------- 3 files changed, 262 insertions(+), 88 deletions(-) diff --git a/aria-actions/README.md b/aria-actions/README.md index d4080251..a6843c22 100644 --- a/aria-actions/README.md +++ b/aria-actions/README.md @@ -6,6 +6,10 @@ Interactive demo of [`aria-actions`](https://pr-preview.s3.amazonaws.com/w3c/ari The demo covers three composite-widget patterns: tabs, a listbox, and a data grid. Naming and structure follow [ARIA PR 1805](https://github.com/w3c/aria/pull/1805) and the [APG Tabs with Action Buttons example](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-actions/). +## What the action buttons do + +Clicking (or AT-invoking) an action toggles a visible state on its target item — Close strikes through the tab, Pin adds a 📌 prefix, Edit italicises the option, Delete strikes it through, View highlights the row, Archive dims the row and rewrites its Status cell. Clicking the same action again reverses the state. Each section has a polite live-region "Last action:" status row that announces what changed, and a **Reset section** button that restores the starting state so the demo is replayable. + ## Learn more - [ARIA spec PR 1805 — rendered preview](https://pr-preview.s3.amazonaws.com/w3c/aria/pull/1805.html) (current normative text) diff --git a/aria-actions/index.html b/aria-actions/index.html index 68ebe655..afd4f096 100644 --- a/aria-actions/index.html +++ b/aria-actions/index.html @@ -134,25 +134,37 @@

    Tabs with Actions

    aria-label="Actions for tabs"> + data-action="close" data-target="index.html" data-target-id="tab-1">Close + data-action="pin" data-target="index.html" data-target-id="tab-1">Pin + data-action="close" data-target="style.css" data-target-id="tab-2">Close + data-action="pin" data-target="style.css" data-target-id="tab-2">Pin + data-action="close" data-target="app.js" data-target-id="tab-3">Close + data-action="pin" data-target="app.js" data-target-id="tab-3">Pin + +
    +

    + Last action: + (none yet) +

    + +
    +
    • Each tab's aria-actions lists two button IDs
    • The buttons sit outside the tablist. role="tab" is @@ -215,31 +227,39 @@

      Listbox with Actions

      + data-action="edit" data-target="MDN Web Docs" data-target-id="opt-1">Edit + data-action="delete" data-target="MDN Web Docs" data-target-id="opt-1">Delete + data-action="edit" data-target="CSS Tricks" data-target-id="opt-2">Edit + data-action="delete" data-target="CSS Tricks" data-target-id="opt-2">Delete + data-action="edit" data-target="Web.dev" data-target-id="opt-3">Edit + data-action="delete" data-target="Web.dev" data-target-id="opt-3">Delete +
      +

      + Last action: + (none yet) +

      + +
      +
      • Each role="option" lists its action button IDs in aria-actions
      • @@ -293,15 +313,15 @@

        Grid with Row Actions

        aria-actions="view-row-1 archive-row-1"> #1024 Alice Johnson - Shipped + Shipped + data-action="view" data-target="order 1024" data-target-id="row-1">View + data-action="archive" data-target="order 1024" data-target-id="row-1">Archive @@ -309,15 +329,15 @@

        Grid with Row Actions

        aria-actions="view-row-2 archive-row-2"> #1025 Bob Chen - Processing + Processing + data-action="view" data-target="order 1025" data-target-id="row-2">View + data-action="archive" data-target="order 1025" data-target-id="row-2">Archive @@ -325,15 +345,15 @@

        Grid with Row Actions

        aria-actions="view-row-3 archive-row-3"> #1026 Carol Davis - Delivered + Delivered + data-action="view" data-target="order 1026" data-target-id="row-3">View + data-action="archive" data-target="order 1026" data-target-id="row-3">Archive @@ -341,6 +361,14 @@

        Grid with Row Actions

        +
        +

        + Last action: + (none yet) +

        + +
        +
        • aria-actions on each role="row" exposes the row's actions
        • @@ -360,13 +388,6 @@

          Grid with Row Actions

          - -
          - diff --git a/aria-actions/style.css b/aria-actions/style.css index c87b754b..4697c2ad 100644 --- a/aria-actions/style.css +++ b/aria-actions/style.css @@ -173,47 +173,139 @@ body { cursor: pointer; } -/* ----- Action announcement toast ----- - Polite live region; visible only when non-empty so it doesn't - clutter the page or trap focus. Replaces window.alert() in the - delegated action-button click handler. - Uses an inverted-surface pairing (text-tone on card-tone) so contrast - is well above WCAG AA in both themes (~14:1 light, ~13:1 dark). */ -.action-toast { - position: fixed; - bottom: 1.5rem; - left: 50%; - transform: translateX(-50%); - max-width: calc(100vw - 2rem); - padding: 0.625rem 1.25rem; - background: var(--text); - color: var(--bg-card); - border: 1px solid var(--text); +/* ----- Per-widget action status & reset bar ----- + The status row is a polite live region (role=status) co-located + with its widget so the announce reaches both AT (via aria-live) + and sighted users (visually). The reset button restores starting + state so the demo stays replayable. */ +.action-status-bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; + margin-top: 1rem; + padding: 0.75rem 1rem; + background: var(--bg); + border: 1px solid var(--border); border-radius: var(--radius); - box-shadow: var(--shadow); - font-size: 0.95rem; - line-height: 1.4; - z-index: 1000; - opacity: 0; - transition: opacity 0.18s ease; - pointer-events: none; } -.action-toast:not(:empty) { - opacity: 1; +.action-status { + margin: 0; + font-size: 0.9rem; + color: var(--text); + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: baseline; + min-width: 0; } -@media (prefers-reduced-motion: reduce) { - .action-toast { - transition: none; - } +.action-status-label { + font-weight: 600; + color: var(--text-muted); +} + +.action-status-message { + font-family: Consolas, Monaco, "Courier New", monospace; + font-size: 0.95em; +} + +.action-reset { + padding: 0.4rem 0.85rem; + border: 1px solid var(--border); + border-radius: 4px; + background: var(--bg-card); + color: var(--text); + font-size: 0.85rem; + cursor: pointer; + white-space: nowrap; +} + +.action-reset:hover { + background: var(--hover-bg); +} + +/* ----- Item state visuals ----- + Action invocations toggle a data-* attribute on the target item; + these rules render the result. Each is reversible (toggle again or + click Reset). Visible to sighted users; the polite status region + above announces the change to AT users. */ + +/* Tabs: Close & Pin */ +.tablist .tab[data-closed] { + text-decoration: line-through; + text-decoration-thickness: 2px; + color: var(--text-muted); +} + +.tablist .tab[data-closed]::after { + content: " (closed)"; + text-decoration: none; + font-weight: 400; + font-size: 0.85em; + color: var(--text); +} + +.tablist .tab[data-pinned]::before { + content: "\1F4CC\00A0"; /* 📌 + nbsp */ + font-size: 0.85em; + vertical-align: 1px; +} + +/* Listbox: Edit & Delete */ +.listbox-option[data-edited] { + font-style: italic; +} + +.listbox-option[data-edited]::after { + content: " (edited)"; + font-style: normal; + font-size: 0.85em; + color: var(--text); +} + +.listbox-option[data-deleted] { + text-decoration: line-through; + color: var(--text-muted); } +.listbox-option[data-deleted]::after { + content: " (deleted)"; + text-decoration: none; + font-size: 0.85em; + color: var(--text); +} + +/* Grid: View & Archive */ +.data-grid tbody tr[data-viewing] { + background: var(--accent-light); +} + +.data-grid tbody tr[data-archived] { + opacity: 0.55; +} + +.data-grid tbody tr[data-archived] .status-cell { + font-style: italic; +} + +/* HCM mappings: don't lose state cues when system colors take over. */ @media (forced-colors: active) { - .action-toast { - background: Canvas; - color: CanvasText; - border-color: CanvasText; + .tablist .tab[data-closed], + .listbox-option[data-deleted] { + /* line-through survives HCM; ensure colour falls back to GrayText. */ + color: GrayText; + } + .data-grid tbody tr[data-viewing] { + background: Highlight; + color: HighlightText; + forced-color-adjust: none; + } + .data-grid tbody tr[data-archived] { + opacity: 1; + color: GrayText; } } From 4a40d96a7f7fd51c58ef25f331c7293616a3c2f2 Mon Sep 17 00:00:00 2001 From: Jacques Newman Date: Fri, 19 Jun 2026 11:16:04 -0700 Subject: [PATCH 05/22] =?UTF-8?q?[aria-actions]=20Co-locate=20tab=20action?= =?UTF-8?q?s=20inside=20.tab-group;=20rename=20Close=E2=86=94Re-open?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layout change: each tab's Close/Pin buttons now sit immediately to the right of the tab they target, inside a per-tab .tab-group wrapper inside the tablist. Matches the APG tabs-actions canonical example's .tab-wrapper pattern. The buttons remain SIBLINGS of the role="tab" button (not children), so Children Presentational on role="tab" is preserved. The wrapper carries role="none" so it doesn't introduce a spurious generic into the tablist's child roles. Renaming: the Close button toggles its visible text and aria-label between "Close" / "Close " and "Re-open" / "Re-open " as data-closed flips on the tab. Pin/Edit/Delete/View/Archive keep static labels — Close is the only action where the next-click verb genuinely differs from the action verb. Reset section restores all Close buttons to "Close" along with the rest of the section state. Notes: - A new HTML comment + notice-list bullet document the axe-core aria-required-children warning this pattern triggers (the tablist has button descendants that aren't role="tab"). aria-actions is precisely what makes the co-location safe; the APG demo triggers the same warning. Treated as a known axe-vs-spec tension, not a defect. axe still reports 9 expected aria-valid-attr FPs on aria-actions itself plus this 1 aria-required-children = 10 total. - Keyboard navigation unchanged: the tablist's keydown handler uses querySelectorAll('[role="tab"]') so ArrowLeft/Right still cycle between tabs only (not action buttons). Verified by puppeteer test "ArrowRight from tab-1 focuses tab-2 (not close-tab-1)". - CSS: .tab-group is inline-flex (align-items: center) holding the tab + .tab-actions-inline cluster. Action buttons are sized down (padding 0.2rem 0.55rem, font 0.8rem) so a tab + 2 actions stays vertically balanced. Selected-tab underline still sits on the tablist's border via margin-bottom: -2px on .tab. - The old single .tab-actions row CSS was removed; its rules are replaced by .tab-actions-inline. Verification: - 16/16 puppeteer-core functional checks (test2.mjs) pass: - Initial Close label + aria-label. - After-close → button textContent "Re-open" + aria-label "Re-open index.html"; tab gets data-closed. - After-re-open → reverts cleanly. - Pin does NOT rename (only Close does). - Reset restores "Close" label even when currently "Re-open". - close-tab-1 is in same .tab-group as tab-1, inside the tablist, but NOT inside [role="tab"]. - ArrowRight from tab-1 focuses tab-2 (not close-tab-1). - axe-core 4.11.4: 10 violations (9 aria-valid-attr on aria-actions + 1 aria-required-children on .tablist — both expected and documented inline). - Visual screenshots in light + dark mode confirm the [index.html] [Close] [Pin] [style.css] [Close] [Pin] [app.js] [Close] [Pin] inline layout and that the selected-tab underline still renders under the tab label (not the whole group). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- aria-actions/index.html | 124 +++++++++++++++++++++++++++------------- aria-actions/style.css | 40 +++++++++---- 2 files changed, 113 insertions(+), 51 deletions(-) diff --git a/aria-actions/index.html b/aria-actions/index.html index afd4f096..399ec5c2 100644 --- a/aria-actions/index.html +++ b/aria-actions/index.html @@ -98,19 +98,64 @@

          Tabs with Actions

          + +
          - - - +
          + + + + + +
          +
          + + + + + +
          +
          + + + + + +
          Tabs with Actions aria-labelledby="tab-3" tabindex="0" hidden>

          Contents of app.js.

          - - -
          - - - - - - -
          -
          + We accept the warning as the cost of co-located action buttons. + + focusgroup="tablist nomemory" gives this tablist declarative + arrow-key navigation (Left/Right with wrap) on browsers that + support the focusgroup attribute. The JS keeps a roving- + tabindex + arrow-key fallback for browsers that don't. + Each tab-actions-inline span carries focusgroup="none" so + the action buttons stay reachable by Tab but are excluded + from the tablist's arrow-key navigation. --> +
          - + - + - +
        @@ -236,10 +253,15 @@

        Listbox with Actions

        +
          + id="bookmarks-listbox" focusgroup="listbox nomemory">
        • MDN Web Docs
        • Listbox with Actions
        • The side panel uses role="group" so its aria-label reaches AT. A bare <div> would swallow the label
        • +
        • Arrow-key navigation uses + focusgroup="listbox nomemory" on the + <ul> when the browser supports it; the + script keeps a roving-tabindex fallback otherwise. The action + panel is a sibling of the listbox, so it's already outside + the focusgroup's arrow-key scope
        @@ -424,6 +452,23 @@

        Grid with Row Actions

        diff --git a/aria-actions/style.css b/aria-actions/style.css index f14d01de..89eb1444 100644 --- a/aria-actions/style.css +++ b/aria-actions/style.css @@ -295,7 +295,7 @@ body { vertical-align: 1px; } -/* Grid: Archive (View is handled by a popover, not row state). */ +/* Grid: Archive (View opens a , not row state). */ .data-grid tbody tr[data-archived] { opacity: 0.55; } @@ -580,10 +580,11 @@ body { background: var(--hover-bg); } -/* ----- Row details popover ----- - Native HTML popover (popover attribute). When open, the browser puts - the element in the top layer and centers it. We just style the panel - itself; positioning is the UA default (auto-centered). */ +/* ----- Row details dialog ----- + Native opened with showModal() — the browser puts the + element in the top layer and centers it via UA defaults. We just + style the panel; positioning is the UA default (auto-centered). + ::backdrop draws the dim layer behind the dialog. */ .row-details-popover { margin: auto; padding: 0;