Skip to content

Round 4: notice, toast and loading - #19

Merged
ecropolis merged 4 commits into
mainfrom
round-4/feedback
Sep 26, 2026
Merged

ecropolis merged 4 commits into
mainfrom
round-4/feedback

Conversation

@ecropolis

@ecropolis ecropolis commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Round 4, chip E (feedback): three elements, one commit each. The patterns come from Rocketbelt's messages, notices, notifications and busy indicators. The markup, CSS and script are new, and nothing named Rocketbelt appears in an id or class. scripts/check-feedback.mjs joins the npm run check chain and covers all three.

notice

What it does. An inline message in four kinds: info, success, warning, danger. Each kind has its own ground, text and accent colours and a Font Awesome Free solid icon, read at build. It takes an optional title, and text or a slot. The icon prop overrides the icon, and false removes it. With dismissible it gets a close button that stays closed for the rest of the visit.

Accessibility. Info and success are role="status"; warning and danger are role="alert". The kind is also said in words: a visually hidden "Warning:" (kindLabel) comes before the message, and the icon is aria-hidden. The close button is a real 44px <button> named "Dismiss". The dismissal is stored in sessionStorage as key = version, and both default to a hash of the content, so new words show again. After a dismiss, focus moves to the next focusable element. Fallback contrast, text on ground and then accent on ground:

Kind Text Accent
info 10.79 5.83
success 8.57 4.86
warning 9.82 4.95
danger 9.35 5.74

Without JavaScript. The notice shows and cannot be closed. The close button stays hidden. The runtime is emitted once per page, and only when a notice is dismissible. Each dismissible notice is mounted by a one-line script straight after it, so a notice closed earlier never flashes. window.__superheroNotice.reset() is there for reviewers.

What the check pins.

  • On the demo page and the gallery index: the role for each kind, no notice rendered hidden, the hidden kind word, and the icon aria-hidden.
  • For a dismissible notice: a close button that is named, type="button" and hidden, a key/version config, the mount script straight after, and the runtime once per page.
  • From the CSS: text on ground at least 4.5:1 and accent at least 3:1 for every kind, and a 44px close button.
  • 10 planted faults it must catch.

Search. inline alert: 260/mo, difficulty 13 (SE Ranking US, 2026-09-26). It also ranks for alert banner (210/12), css alert box (260/38) and callout box (330/17). The pre-assigned "alert banner component" has no US volume, and the banner across the top of a site is announcement-bar.

Theming prefix. --ntc-*, with classes .ntc and .ntc__* and the attributes data-ntc and data-ntc-close. Commit e488031 renamed these from --nt-* and data-nt, which news-ticker already owned on main, so theming leaked between the two elements. Toast uses --ts-* and .tst, and loading uses --ld-* and .ld. Neither is shared with any other element.

Attribution. license: MIT. Pattern from Rocketbelt (Pier 1 Imports, 2020, MIT); reimplemented, no code copied.

toast

What it does. Put one <Toast /> on the page. It renders the region, fixed at position (bottom-right by default). A second <Toast /> renders nothing and warns at build. window.__superheroToast.show({ text, kind?, action?, timeout? }) returns { element, dismiss(), open }. data-toast buttons do the same with no script of their own; the demo uses them.

Accessibility.

  • The region is role="region" with aria-live="polite", named "Notifications". Success, warning and danger toasts say their kind in words; icons are aria-hidden.
  • Timers pause on hover, on focus and while the tab is hidden, and resume with the time they had left.
  • A toast with an action never leaves by itself. timeout: 0 keeps a toast until it is closed. A timeout under 4 s is raised to 4 s.
  • At most three show at once. A fourth pushes the oldest out.
  • Escape closes the toast that has focus. Focus then moves to the next toast, or back to where it was before, never to <body>.
  • Each toast has a 44px close button.
  • Text is set as text, never parsed as HTML. An action link with a scheme other than http(s), mailto or tel, such as javascript:, is refused.
  • Under reduced motion there is no slide: toasts appear and leave at once.
  • A bottom corner rises above the cookie-consent bar while that bar shows.

Without JavaScript. Nothing renders. The region is hidden until its script mounts it, and a toast only exists because a script made one. The catalogue says so plainly and points to notice for anything that must exist without JavaScript.

What the check pins.

  • On both pages: exactly one region, polite and named "Notifications", rendered hidden and empty, with aria-hidden icon templates, the runtime straight after it and emitted once.
  • The runtime itself: toastRuntime() between the <tst-runtime> markers is the same code serialised into the page. The check runs it on a DOM stand-in with a fake clock and covers:
    • text staying text
    • the default timeout and per-toast timeouts, and the 4 s floor
    • pausing on hover, on focus and in a hidden tab
    • actions: the button, onClick, the link, and refused hrefs
    • the three-toast cap
    • Escape and where focus goes next
    • data-toast buttons
    • motion and reduced motion
  • Contrast of the fallbacks: text 14.75, action 8.01, and kind icons 7.74 to 10.23 to 1.
  • Animation only under no-preference.
  • 15 planted faults it must catch, including 9 in the runtime.

Search. toast notifications: 320/mo, difficulty 32. It also ranks for toast notification (1,100/67), toast popup (210/33) and html toast (70/26). The pre-assigned "toast notification html" has no US volume.

Attribution. license: MIT. Pattern from Rocketbelt (Pier 1 Imports, 2020, MIT); reimplemented, no code copied.

loading

What it does. One element with three shapes:

  • spinner: an inline SVG ring with role="status" and a visually hidden label (default "Loading"). showLabel shows the label instead, and size is sm, md or lg.
  • skeleton: lines, avatar or card placeholders drawn from CSS gradients, with a shimmer.
  • busy: a wrapper for real content. While it has data-busy, its content is aria-busy="true" and inert, and the spinner covers it after 300 ms, so fast actions never flicker. slowText appears after slowAfter ms, and doneLabel is said when the wait ends. Hosts toggle the attribute or call window.__superheroLoading.busy(el, on), where el is the wrapper, anything inside it, or a selector.

Accessibility.

  • The busy status line is a sibling of the content, deliberately. Screen readers hold back changes inside an aria-busy region, so a message inside it might never be read.
  • If focus was inside the content, it moves to the wrapper while busy, then back to the same control.
  • Skeleton shapes are aria-hidden. With label, a visually hidden status speaks for the skeleton instead.
  • Under reduced motion the ring stops turning and fades gently, and the skeleton does not shimmer.
  • In forced-colours mode the ring and the skeleton shapes use the system text colour.

Without JavaScript. A spinner and a skeleton render as given. A busy wrapper renders its content, usable. The overlay, aria-busy and inert only ever come from the script, which is emitted once per page and only when a busy wrapper is on it.

What the check pins.

  • Every spinner is a named role="status" with an aria-hidden ring.
  • Every skeleton either is wholly aria-hidden or carries a named status, and its shapes are aria-hidden.
  • Every busy wrapper renders with no inert and no aria-busy, an aria-hidden overlay, and an empty status outside the content.
  • The runtime appears once with a busy wrapper and never without one.
  • The busy runtime runs on the DOM stand-in, which gains a MutationObserver so the bare-attribute path is tested too. It covers:
    • aria-busy placed on the content, not the wrapper
    • inert on the content
    • holding focus on the wrapper and giving it back
    • the slow message
    • doneLabel
    • false for a target outside any wrapper
  • From the CSS: animation only inside the motion blocks, where the reduce branch may only fade; the overlay behind .ld--live; the slow message at 14.75:1.
  • 15 planted faults it must catch.

Search. css loading animation: 480/mo, difficulty 18. It also ranks for css skeleton (390/27), skeleton ui (480/23) and loading spinner (720/33). The pre-assigned "skeleton loader css" has no US volume, and "skeleton loader" is 20/45.

Attribution. license: MIT. Pattern from Rocketbelt (Pier 1 Imports, 2020, MIT); reimplemented, no code copied.

Verification

  • npm run check passes after each commit, and npm run build passes.
  • The website type check against this branch passes with 0 errors and 0 warnings. That is a scratch clone of ecropolis/SuperheroTech main running ELEMENTS_SOURCE=<this checkout> npm run check:types. Its build produces /elements/notice/, /elements/toast/ and /elements/loading/, and check:elements passes. Every executable inline script on those pages is covered by the CSP hashes.
  • In headless Chrome, with and without reduced motion:
    • notice: dismiss, remembered across a reload, reset, focus moving on
    • toast: kinds, pause on hover and resume, the three-toast cap, Undo from the keyboard, Escape and where focus goes
    • loading: busy and inert, focus held and given back, the slow message, the attribute toggle, the selector form
    • no console errors, and no sideways scroll at 360px on the gallery index or any of the three pages
  • Search queries: none clashes with any existing query or alsoRanks, and check-catalog enforces that.

Not in this PR

The three sections for the superhero-ui-library skill in ecropolis/agent-skills, with the plugin version bump. The README counts that as part of "finished". Chips D, F and G bump the same plugin version, so the skill update is best done once, after the round merges.

🤖 Generated with Claude Code

ecropolis and others added 4 commits September 26, 2026 11:27
…visit

One file, src/library/notice/Notice.astro. kind info | success | warning | danger, each
with its own ground, text and accent and a Font Awesome Free solid icon read at build
(circle-info, circle-check, triangle-exclamation, circle-exclamation; `icon` overrides,
false drops it). Optional title; text or a default slot.

role="status" for info and success, role="alert" for warning and danger, and the kind is
said in words too: a visually hidden "Warning:" (kindLabel) leads the message. dismissible
adds a 44px close button named "Dismiss", remembered in sessionStorage as key = version
(both default to a hash of the content, so new words show again); after a dismiss focus
moves to the next focusable thing. The runtime is emitted once per page and only when a
notice is dismissible; each dismissible notice is mounted by a one-line script straight
after it, so one closed earlier in the visit never flashes. window.__superheroNotice.reset()
is for reviewers. Without JavaScript the notice shows and the close button stays hidden.

Fallback contrast, text / accent on ground: info 10.79 / 5.83, success 8.57 / 4.86,
warning 9.82 / 4.95, danger 9.35 / 5.74.

Search: "inline alert" (SE Ranking US 260/mo, difficulty 13). The pre-assigned "alert
banner component" has no US volume; "alert banner" (210/12) goes in alsoRanks.

scripts/check-feedback.mjs joins npm run check: roles per kind, the no-JS render, mount
order and a single runtime on the demo page and the gallery index, contrast per kind from
the CSS, the 44px close, and 10 planted faults it must catch.

Catalogue license: MIT. Pattern from Rocketbelt (Pier 1 Imports, 2020, MIT);
reimplemented, no code copied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One file, src/library/toast/Toast.astro. Put one <Toast /> on the page; it renders the
region (role="region", aria-live="polite", "Notifications", fixed at `position`), hidden
until its script mounts it. A second <Toast /> renders nothing and warns at build.

window.__superheroToast.show({ text, kind?, action?, timeout? }) returns { element,
dismiss(), open }; data-toast buttons do the same without a script of their own. Text is
set as text. kind info | success | warning | danger picks the Font Awesome Free icon
(read at build into <template>s) and a hidden word ("Error: "). action is a button
(onClick(event, handle)) or a link; an href with a scheme other than http(s), mailto or
tel is refused. Timers pause on hover, on focus and in a hidden tab and resume with what
they had left; a toast with an action never leaves by itself; 0 keeps it; under 4 s is
raised to 4 s. At most three: a fourth pushes the oldest out. Escape closes the focused
toast, and focus moves to the next toast or back to where it was. Under reduced motion
toasts appear and leave at once. A bottom corner rises above the consent bar.

Without JavaScript nothing renders, and the catalogue says so: use a notice for anything
that must exist without it.

One copy of the runtime: toastRuntime() between the <tst-runtime> markers is serialised
into the inline script, and check-feedback.mjs runs it on a DOM stand-in with a fake
clock (text, timeouts and the floor, pauses, actions, the cap, Escape and focus, refused
hrefs, data-toast, motion), pins the page (one hidden region, empty, templates
aria-hidden, the runtime once), measures the fallbacks (text 14.75, action 8.01, icons
7.74 to 10.23 to 1) and plants 15 faults it must catch.

Search: "toast notifications" (SE Ranking US 320/mo, difficulty 32). The pre-assigned
"toast notification html" has no US volume.

Catalogue license: MIT. Pattern from Rocketbelt (Pier 1 Imports, 2020, MIT);
reimplemented, no code copied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One file, src/library/loading/Loading.astro, three shapes:

- spinner: an inline SVG ring, role="status" with a visually hidden name (`label`,
  default "Loading", or shown with showLabel), sizes sm / md / lg.
- skeleton: lines, avatar or card placeholders drawn with CSS gradients and a shimmer.
  The shapes are aria-hidden; with `label` a hidden role="status" says it instead.
- busy: a wrapper that, while it has data-busy, makes its content aria-busy="true" and
  inert, covers it with the ring (after 300 ms, so fast actions never flicker) and says
  `label` in a status line outside the busy content, because screen readers hold back
  changes inside an aria-busy region. Focus inside the content moves to the wrapper and
  back to the same control afterwards. slowText after slowAfter ms, doneLabel when done.
  Hosts toggle data-busy or call window.__superheroLoading.busy(el, on) (el, anything in
  it, or a selector). The runtime is emitted once per page, only with a busy wrapper.

Reduced motion: the ring stops and fades gently, the skeleton is flat, the overlay does
not fade. Without JavaScript a spinner and a skeleton render as given and a busy wrapper
renders its content, usable: overlay, aria-busy and inert only come from the script.

check-feedback.mjs pins the no-JS render of every shape on the demo page and the gallery
index, runs the busy runtime (between the <ld-runtime> markers) on the DOM stand-in, now
with a MutationObserver so the bare attribute path is tested, confines animation to the
motion blocks (the reduce branch may only fade), keeps the overlay behind .ld--live, and
measures the slow message (14.75:1); 15 planted faults for loading, 40 in all.

Search: "css loading animation" (SE Ranking US 480/mo, difficulty 18). The pre-assigned
"skeleton loader css" has no US volume.

Catalogue license: MIT. Pattern from Rocketbelt (Pier 1 Imports, 2020, MIT);
reimplemented, no code copied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--nt-* and data-nt are news-ticker's, already on main: a host theming one
themed the other, and each element's script could find the other's root
on a page that has both. Following the library's rule when two letters
are taken (--anb-, --ofc-), notice is now ntc in its custom properties
(including the private --_ntc-accent), classes, data attributes, the
element expando and the demo, and check-feedback follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ecropolis
ecropolis merged commit cbdd340 into main Sep 26, 2026
2 checks passed
@ecropolis
ecropolis deleted the round-4/feedback branch September 26, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant