Skip to content

Round 4: tabcordion, tooltip and responsive-table - #24

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

ecropolis merged 4 commits into
mainfrom
claude/round-4-structure

Conversation

@ecropolis

@ecropolis ecropolis commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Chip D of library round 4: three structure elements, reimplemented from Rocketbelt's patterns. One commit per element. Each has one self-contained .astro file, a demo, a catalogue entry, and a section in the new scripts/check-structure.mjs, which is now in npm run check.

The check follows the business-hours pattern. Each element's rules live in one plain-JS block between // <name> markers. The check takes that block from the built page, requires it to equal the source, and runs it against golden cases. It then pins the no-JS render of the built demo page. Finally it runs mutation tests: every check is run against deliberately broken copies (render, CSS and logic), and must fail each one.

tabcordion

What it does. One set of panels. It is tabs when the element itself is wide and an accordion when it is narrow. A CSS container query on the element's own width decides, not the window. So the same element is tabs in a wide column and an accordion in a 17rem sidebar on a desktop page. The container query sets --tcd-layout; the script reads it on resize and never measures a width itself, so the breakpoint lives only in CSS. State carries across the switch. Going to the accordion opens the selected tab's panel. Going to tabs selects the panel opened most recently that is still open. The accordion holds several panels open unless multiple={false}.

When to pick something else. notFor names both neighbours. Use tabs when the set should stay tabs until the phone, or needs a vertical strip, icons, manual activation or a hash that follows the selection. Use accordion for questions, FAQ schema, or answers that find-in-page should reach while closed.

Accessibility contract.

  • Tabs layout: a tablist named by label, with tab and tabpanel, aria-selected, aria-controls, aria-labelledby and a roving tabindex. ← → wrap; Home and End jump to the ends.
  • Accordion layout: each heading holds a real button with aria-expanded and aria-controls; ↑ ↓ Home End move between headings. The strip is hidden and the panels lose the tabpanel role.
  • Motion: under reduced motion there is no fade and no chevron turn.

No-JS render. The strip is hidden and every panel is open under its heading.

What the check pins.

  • Golden cases for the state across the breakpoint, the keys, and the full ARIA view on both sides.
  • The no-JS render: the strip is hidden, every panel is open, each heading equals its tab's label, and aria-controls and ids line up.
  • No role and no aria-expanded before the script runs, and the runtime emitted once per page.
  • The three @container rules, with no matchMedia or innerWidth in the script.
  • 17 mutants caught.

Search. Query "responsive accordion": 320/mo, difficulty 32, and rising (50/mo a year ago). alsoRanks: "tabs on mobile" (110/11), "responsive tabs" (40/35), "tabs to accordion" (10/34). The brief's "responsive tabs accordion" and "tabcordion" have no measurable volume. Source: SE Ranking US, 2026-09-26.

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

tooltip

What it does. A real <button>, or a link with href, whose aria-describedby names a role="tooltip" of plain text.

  • Placement: top, bottom, left or right. It is measured on show, flips when its side has no room, and slides to stay 8px inside the window. The arrow keeps pointing at the trigger.
  • Top layer: where the browser has the Popover API, the tip shows in the top layer, so no overflow: hidden ancestor clips it.
  • No markup: markup in text fails the build. Anything interactive belongs in a popover, and notFor says so.

Accessibility contract (WCAG 1.4.13).

  • It shows on focus as well as hover, never hover only.
  • Escape hides it without moving focus or the pointer.
  • The pointer can cross the gap onto the tip without losing it.
  • It stays until both pointer and focus have left.
  • Taps toggle it on touch, and one tip shows at a time.
  • Icon-only triggers take label, their icon is aria-hidden, and they are 44 × 44 px.
  • The fallback colours are 16.0:1.
  • There is no fade under reduced motion.

No-JS render. The text shows in brackets after the trigger. With inline={false} it is the trigger's title instead, and the script removes that title when it runs.

What the check pins.

  • 22 state cases, for example: focus alone shows it; Escape while focused hides it; crossing the gap onto the tip keeps it; second tap hides it.
  • 13 placement cases: flips, the most-room fallback, slides, and the arrow clamp.
  • The no-JS render: aria-describedby equals the tip's id, role="tooltip", no interactive content in the tip, and the inline and title variants.
  • Icon-only triggers: aria-label and the aria-hidden wrapper.
  • The focus and Escape wiring in the script, the 44px rule, the fade inside the reduced-motion query, and the fallback contrast.
  • 24 mutants caught.

Search. Query "tooltip html": 320/mo, difficulty 24. alsoRanks: "accessible tooltip" (50/14; the brief's query, kept as a variant of the bigger one), "tooltip ui" (260/36), "html tooltip" (10/36). Source: SE Ranking US, 2026-09-26.

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

responsive-table

What it does. Columns and rows go in; a <table> comes out, with a <caption>, scope="col" and scope="row". Numeric columns are detected at build time and right-aligned with tabular figures.

  • Cards (default): below a breakpoint on the element's own width, each row becomes a card. Every value sits under its column's name, drawn from data-label in CSS only.
  • Scroll mode: a focusable region named by the caption. Edges fade where there is more, a hint shows until the first scroll, and the caption text stays in view. The box is no Tab stop when nothing overflows.
  • Options: striped rows, and a sticky header that sticks inside the box when maxHeight is set.
  • Not for layout: notFor says so.

Accessibility contract.

  • Explicit table, rowgroup, row, columnheader, rowheader and cell roles keep table semantics when cards restyle the rows. Verified in Chromium's accessibility tree: the card rows are still rows with a rowheader and cells.
  • Card labels use content: attr(data-label) / '', so a screen reader hears the column header once, not twice.
  • The scroll hint is aria-hidden.

No-JS render. Cards mode has no script at all. A scroll-mode box scrolls without its fades or hint.

What the check pins.

  • 14 numeric-detection cases: the frontmatter block with its types stripped.
  • 9 scroll-edge cases, right-to-left included.
  • The render: caption, scope and roles on every part, and each data-label equal to its header's text.
  • Numeric alignment consistent per column and agreeing with the detection.
  • The scroll region's name and tabindex, and the hint hidden and aria-hidden.
  • CSS-only cards at each size, with the alt-text labels.
  • 23 mutants caught.

Search. Query "responsive tables": 140/mo, difficulty 8. alsoRanks: "responsive table html" (110/25; the brief's query), "responsive table css" (110/0), "responsive data table" (70/16), "sticky table header" (50/12). The whole cluster is down from about 260 a year ago. Source: SE Ranking US, 2026-09-26.

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

Verification

  • npm run check and npm run build are green.
  • The website's type check passes against this branch: a scratch clone of ecropolis/SuperheroTech main, with ELEMENTS_SOURCE=<this checkout> npm run check:types, gave 0 errors and 0 warnings. None of its hints are in the new files.
  • The site's npm run build and npm run check:elements also pass. All three pages mount with no console errors after its CSP step moves the inline scripts out.
  • In headless Chromium (Playwright):
    • tabcordion: the ARIA follows both directions of the switch, the keys work, and the hash opens a panel.
    • tooltip: hover, focus, Escape, crossing onto the tip, flips at the edges, taps on touch, and 44px icon targets all work.
    • responsive-table: cards appear below 40rem, the fades and hint update as it scrolls, and the sticky header stays inside the box.
    • The no-JS render and reduced motion were checked for each.

Notes

  • No new Element fields, and no source.
  • Prefixes: tabcordion uses --tcd-*, .tcd__* and data-tcd. It first shipped as --tc-* and data-tc, which testimonial-carousel already owns, so its boot picked up carousel roots and threw (renamed in 2de7f40). tt (tooltip) and rt (responsive-table) collide with nothing in the library.
  • There are no Rocketbelt or Pier 1 names in any id or class; the licence lines are the only mention.
  • Merge conflicts: the other round-4 chips also append to catalog.ts, demos/index.ts and the npm run check chain in package.json, so expect trivial conflicts there. Both sides are kept.
  • Skill sections: the three superhero-ui-library sections are in a separate agent-skills PR.

🤖 Generated with Claude Code

ecropolis and others added 4 commits September 26, 2026 11:31
…rrow

One set of panels, one DOM. A CSS container query on the element's own width sets
--tc-layout; the script reads it on every resize and makes the ARIA match
(tablist/tab/tabpanel, or heading buttons with aria-expanded). The selected tab's panel
is open in the accordion and the panel opened last is the tab when it widens; the
accordion holds several open unless multiple={false}. No-JS: every panel open under its
heading, the strip hidden.

scripts/check-structure.mjs (new, in npm run check) runs the shipped state block against
golden cases, pins the no-JS render and the one-breakpoint-in-CSS rule, and must catch 17
mutants of the render, the stylesheet and the logic.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… on Escape

A real button (or link) whose aria-describedby names a role="tooltip" of plain text. It
shows on focus as well as hover, the pointer can move onto it, Escape dismisses it, taps
toggle it, and one shows at a time (WCAG 1.4.13). Placement top/bottom/left/right is
measured on show: it flips when the side has no room and slides to stay 8px inside the
window, in the Popover API's top layer where the browser has it. Icon-only triggers take
`label` and are 44px. No-JS: the text in brackets after the trigger, or its title with
inline={false}. Markup in `text` fails the build.

check-structure.mjs runs the shipped state and placement blocks against 22 + 13 golden
cases, pins the no-JS render, the focus and Escape wiring, the 44px target and the
fallback contrast (16.0:1), and must catch 24 mutants.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts column is narrow

Columns and rows in, a <table> out: caption, scope="col"/"row", explicit table roles (so
cards keep table semantics in Chrome and Safari), and numbers right-aligned with tabular
figures, detected at build time. mode="cards": below a breakpoint on the element's own
width each row becomes a card, every value under its column's name from data-label, CSS
only, the label drawn with empty alt text so it is not read twice. mode="scroll": a
focusable region named by the caption, edge fades where there is more, a one-time hint,
and no Tab stop when nothing overflows. Optional striped rows and a sticky header
(inside the box with maxHeight). No-JS: the table.

check-structure.mjs runs the numeric detection (frontmatter, types stripped) and the
shipped edge maths (right-to-left included) against 14 + 9 golden cases, pins caption,
scope, roles, data-label = header text, numeric alignment, the scroll region and hint,
and the CSS-only cards at each size, and must catch 23 mutants.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--tc-* and data-tc are testimonial-carousel's, already on main. Beyond
theming, the clash broke pages: tabcordion boots with
querySelectorAll('[data-tc]'), found the carousel's root, and threw on its
missing tab list inside forEach, so no tabcordion after a carousel on the
page mounted (the gallery index has both, carousel first). Following the
library's rule when two letters are taken (--anb-, --ofc-), tabcordion is
now tcd in its custom properties (--tcd-layout included), classes, the
runtime-built toggle class, data attributes and the demo, and
check-structure follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ecropolis
ecropolis merged commit 93cfb04 into main Sep 26, 2026
2 checks passed
@ecropolis
ecropolis deleted the claude/round-4-structure 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