Workshop Kit · widget reference
Shippedtab-flip
A WAI-ARIA tabs widget for inline rhetorical comparisons. The Method uses it for "good copy vs bad copy" side-by-sides — three response styles to a bad review, four versions of a one-promise sentence. Viewer-only: the operator reads, picks, learns. No state writes to MuntinContext.
Live demo
Tabs render in the order specified; the defaultIndex determines which one opens first. Click a tab, use Left/Right arrow keys to preview labels, press Enter or Space to switch the panel. Manual-activation focus mode means a screen-reader user can walk the labels without committing to a panel change.
Three response styles · defaultIndex: 2 (the "good" tab opens first)
No MuntinContext writes — tab-flip is a viewer-only widget. The operator's choice doesn't persist; reading-and-comparing IS the learning.
Contract
The widget reads its tabs from an inline JSON config block. The lesson author authors the tab labels + panel HTML; both fields take optional -es variants for the Spanish copy. No data attributes beyond data-widget.
| Attribute / config | Type | Purpose |
|---|---|---|
data-widgetRequired |
literal | Always "tab-flip". |
<script class="tab-flip-config" type="application/json">Required |
JSON object | { tabs: [...], defaultIndex: 0 }. Each tab is { id, label, label-es, body, body-es }. id is the stable identifier (used for the panel's aria-controls); label renders on the tab trigger; body is the HTML rendered inside the panel. |
tabs[*].body |
HTML string | Rendered as-is into the panel — lesson authors control the safety of this content (these are lesson assets, not operator input). Keep it small; prefer single-paragraph examples with optional <blockquote> for the contrast subject and <p class="meta"> for the rhetorical takeaway. |
defaultIndex |
integer | Zero-based tab index that opens first. Use the "good example" position when the lesson is teaching the right answer; use the "bad example" position when the lesson is teaching the failure mode first. |
What it writes: nothing. tab-flip is viewer-only; contextKeys is the empty array.
Markup
Accessibility
Full WAI-ARIA Tabs Pattern implementation:
role="tablist"on the trigger container with horizontal orientation.role="tab"+aria-selected+aria-controlson each trigger button.role="tabpanel"+aria-labelledby+tabindex="0"on each panel.- Manual activation: arrow keys move focus across tab labels WITHOUT switching the panel. Enter or Space commits to the focused tab. This lets screen-reader users preview labels without panel content noisily re-rendering on every arrow press.
- Home / End jump to first / last tab.
- Inactive tabs have
tabindex="-1"so a Tab key step jumps over the unfocused triggers into the active panel.
Where it ships
- L3 — Your one promise (three promise styles: vague / poetic / specific)m1-orient/one-promise
- L6b — Diagnose what's leaking (good vs bad copy frame)m2-decide/rebuild/leaks
- L9b — Photo refresh triage (good vs bad photo frame)m3-assemble/rebuild/photos
- L13 — Reviews + first-week trust (three response styles)m4-launch/reviews
Source
/tools/_shared/workshop/tab-flip.js — ~180 LOC. Exports { tag, contextKeys, mount, serialize } per the Workshop Kit widget contract. Parses the inline JSON config, renders the tab strip + panels, wires the WAI-ARIA Tabs Pattern keyboard model (arrows for focus, Enter/Space to activate, Home/End for edges).
contextKeys is the empty array — tab-flip never writes to MuntinContext. The operator's tab choice is ephemeral (resets on reload). That's deliberate: the learning IS the comparison, not the choice.