Repository navigation
Round 4: menu-button, radio-group and stepper - #22
Merged
Merged
Conversation
A <button aria-haspopup="menu" aria-expanded aria-controls> that opens a role="menu" of links or buttons (menuitemradio with aria-checked for a setting such as Sort by). ↓ ↑ wrap, Home and End jump, a letter moves to the next item starting with it, Enter and Space activate, Escape closes and returns focus to the button, Tab closes and moves on; a click outside or focus leaving closes it; disabled items stay focusable as aria-disabled. It opens above the button when the room below is short (measured on each open) and shifts to the other edge rather than leave the viewport. Activating an item dispatches a cancelable, bubbling data-selected. Without JavaScript the same items sit in a closed <details><summary>, and the button is rendered hidden; menu roles appear only once the script can honour their keys. notFor points site navigation at mega-menu and form values at <select>. Search: "menu button" (720/mo, difficulty 23). scripts/check-controls.mjs joins npm run check: the no-JS render, the roles the emitted script sets, and the <mb-keys> block run from the built page against the keyboard contract and the placement rule; FA Free paths, fallbacks and contrast; and ten mutants of the page, every one caught. Pattern from Rocketbelt (Pier 1 Imports, 2020, MIT); reimplemented, no code copied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
<input type="radio">s in a <fieldset> named by its <legend>, never replaced
or given a role, in three looks: default (a styled list), chunky (a card per
option with a Font Awesome Free icon, a title and a line of text, which is
the radio's description) and segmented (a pill row). The keyboard is the
browser's; the focus ring is 3px on the radio or around the card or
segment; states come from :checked sibling selectors, with a Highlight
outline in forced-colors mode. It posts name=value in a plain form, and
`required` is the browser's own validation, so without JavaScript nothing
is missing. The one script (once per page) sets data-value on the fieldset
and dispatches a bubbling data-changed with { name, value, label }.
Chunky icons are Font Awesome Free names read at build from
@fortawesome/fontawesome-free, as the icon element does, lazily and only
when an option has one; an <svg> string is accepted for a site without the
package. Search: "radio button css" (590/mo, difficulty 21); the
pre-assigned "segmented control css" has no measurable volume, so
"segmented control" (260/7) is a variant.
check-controls.mjs gains the radio-group section: names, ids, values, one
checked, labels and descriptions resolving to text, no role or tabindex,
the demo form's post computed as a browser would send it, FA Free icons,
contrast, and eight mutants, every one caught.
Pattern from Rocketbelt (Pier 1 Imports, 2020, MIT); reimplemented, no code
copied.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…JavaScript An ordered list of steps joined by connector lines, horizontal from 40rem of its own width and vertical below (a container query, so a sidebar gets the vertical layout too; orientation="vertical" always stacks). The current step's <li> carries aria-current="step"; a done step says "Completed:" to screen readers, shows a Font Awesome Free check and links back when it has an href; current and upcoming steps never link. It is a <nav> named by `label` when any step can link, and a named list otherwise. What the build renders is right without JavaScript. The one script (once per page) defines window.__superheroStepper.go(id, n) for a form that advances in place: it clamps n, moves the states, aria-current, the hidden text and the links, and moves no focus (the form should focus its next section's heading). Search: "step indicator" (210/mo, difficulty 12); the pre-assigned "progress steps html" has no measurable volume. check-controls.mjs gains the stepper section: the <st-states> rule run from the built page and compared with every rendered stepper, aria-current on exactly the current step, "Completed:", checks and links on done steps only, the go() clamp, contrast, and seven mutants, every one caught. Pattern from Rocketbelt (Pier 1 Imports, 2020, MIT); reimplemented, no code copied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Chip F of library round 4: three controls whose patterns come from Rocketbelt, reimplemented in the library's shape. Each is one self-contained
.astrofile with no runtime dependencies and no imports of other elements. Each has--<prefix>-*theming with fallbacks, a keyboard and screen-reader contract, reduced motion, a sensible no-JS render, a demo, and a catalogue entry withpitch,usedOnand a uniquesearch.query. Icons are Font Awesome Free only. There is one commit per element, andnpm run checkpasses at each commit.scripts/check-controls.mjsjoins thenpm run checkchain. It reads each element's built demo page and the gallery index, which renders every demo on one page. It pins the no-JS render, the roles and keyboard contract in the emitted script, and each once-per-page runtime. It also requires inlined<path>s to be Font Awesome Free byte for byte, one fallback per CSS variable, and contrast on the fallback colours. It then runs a mutation test: 25 deliberately broken copies of the pages, and every one must be caught.menu-button
What it does. A button that opens a short menu of actions or links: Share, Export, Sort by, the ⋮ on a card. Items are links, or buttons that dispatch a cancelable, bubbling
data-selectedevent with{ value, label, href }.mode="radio"makes a settings menu withmenuitemradioandaria-checked.iconOnlygives a 44px ⋮ button, andalignsetsstartorend.notForsends site navigation tomega-menuand choosing a form value to<select>.Accessibility contract. It follows the WAI-ARIA Authoring Practices menu button.
aria-haspopup="menu",aria-expandedandaria-controls. The list isrole="menu"labelled by the button, withrole="none"on each<li>andtabindex="-1"on each item.aria-disabled: they stay focusable but cannot be activated.No-JS render. The same items sit in a closed
<details><summary>with the same label, and link items work. The real button is renderedhidden. No menu role is rendered until the script can honour its keys.What the check pins.
<details>fallback is present and closed, and the summary and button carry the same name.aria-haspopup="menu"andaria-expanded="false", and itsaria-controlsnames the list.roleortabindex, and button items aretype="button".aria-checked,aria-disabledandaria-expanded, dispatchesdata-selected, closes on outside pointerdown and focusout, and returns focus to the button.<mb-keys>block is run from the built page against the keyboard contract (18 cases), the opening keys and the placement rule.menuitemradiodropped, focus not returned, a wrong flip rule and a doubled runtime.Search. The target is
menu button: 720/mo, difficulty 23.alsoRanksholdsdropdown button(190/27),action menu(170/6) anddropdown menu button(10/60). The last is the pre-assigned phrase, too small to target. All figures are SE Ranking US, 2026-09-26.Attribution. MIT. Pattern from Rocketbelt (Pier 1 Imports, 2020, MIT); reimplemented, no code copied. Rocketbelt's dropdown is a listbox-style select, so only its intent carried over: keyboard navigation, Escape, click outside and decoration after load.
radio-group
What it does. One choice from a few, as real
<input type="radio">s in a<fieldset>with a<legend>. There are three styles:defaultis a styled list.chunkyis a card per option, with a Font Awesome Free icon, a title and a line of text.segmentedis a pill row.Options are
{ value, label, text?, icon?, disabled? }. Other props arevalue,name,required,hintandhideLegend, plus--rg-*tokens. For hosts, the script setsdata-valueon the fieldset and dispatches a bubblingdata-changedevent with{ name, value, label }. Chunky icons are Font Awesome Free names read at build time from@fortawesome/fontawesome-free, as the icon element does, and only when an option has one. An<svg>string is accepted for a site without the package.Accessibility contract.
aria-describedby. The hint describes the group.No-JS render. It is a form control and fully works: it posts
name=valuein a plain form, withrequiredvalidation from the browser. The catalogue says so.What the check pins.
aria-labelledbyandaria-describedbyresolves to text, and there is noroleortabindex.delivery=pickup&billing=monthly, and every contact radio carriesrequired.data-changedis dispatched, and the runtime appears once per page.requireddropped, the legend removed and the event renamed.Search. The target is
radio button css: 590/mo, difficulty 21.alsoRanksholdsradio group(480/30),radio button design(390/28),segmented control(260/7, volatile) andstyled radio buttons(210/22). The pre-assignedsegmented control csshas no measurable volume.Attribution. MIT. Pattern from Rocketbelt (Pier 1 Imports, 2020, MIT); reimplemented, no code copied.
stepper
What it does. An
<ol>of steps, each done, current or upcoming, joined by connector lines.orientation="vertical"always stacks.hrefandtext.currentis the 1-based step number, andsteps.length + 1means every step is done.window.__superheroStepper.go(id, n)advances it for a form that does not reload.Accessibility contract.
aria-current="step"is on exactly the current<li>, and on none once every step is done.href. Current and upcoming steps never link.<nav>named bylabelwhen any step can link. Otherwise the list itself is named, so a purely visual stepper adds no landmark.go()moves states,aria-current, the hidden text and the links. It moves no focus: the form should focus its next section's heading.No-JS render. It is static as rendered, which is right for the common case of one page per step.
What the check pins.
<st-states>rule is run from the built page against golden cases. Every rendered stepper's states must equal that rule for itsdata-current.aria-currentis exactly"step"and on exactly the current step. Done steps carry "Completed:", the check and the right link. No other step links.<nav>exactly when a step links, and markers arearia-hidden.go()clamp and thearia-currentmoves are in the emitted script, and the runtime appears once per page.aria-currentremoved, set topage, or duplicated, the state rule shifted by one, a link on the current step, "Completed:" dropped and the clamp removed.Search. The target is
step indicator: 210/mo, difficulty 12.alsoRanksholdswizard steps(170/12),progress indicator(260/42) andsteps ui(90/7). The pre-assignedprogress steps htmlhas no measurable volume, andstepperalone (14,800/78) is mostly exercise machines.Attribution. MIT. Pattern from Rocketbelt (Pier 1 Imports, 2020, MIT); reimplemented, no code copied.
Verification
npm run checkpasses at each of the three commits, andnpm run buildpasses.ELEMENTS_SOURCE=<this checkout> npm run check:types. It reports 0 errors and 0 warnings, and none of its 12 hints are in the new files.astro buildthere also renders/elements/menu-button/,/elements/radio-group/and/elements/stepper/, with the chunky icons resolved from the vendored path.<details>opens, the form posts?delivery=pickup&billing=yearly&contact=phone, andrequiredblocks the post;go(), including its clamp and unknown ids;Elementfields were added and nosourcewas set.Merge note. The sibling round-4 PRs also append to the end of
catalog.tsanddemos/index.tsand extend thecheckchain inpackage.json. Whichever merges second gets a trivial conflict there: keep both sides.🤖 Generated with Claude Code