Problem
The scaffold gallery was recently made internally consistent on the design system (a single section-spacing rhythm, themed tokens, and one shared focus ring; see PR #1060). The other in-repo WebJs apps (website/, examples/blog/, docs/, packages/ui/packages/website/) were NOT swept the same way and have drifted.
The concrete trigger was the focus ring. An interactive element that does NOT go through the design system's global focus rule falls back to the raw browser outline: auto (thick, and light-coloured on a dark theme), which shows on window-refocus (:focus-visible re-fires when the OS window regains focus). Observed on the website's live like-button and the copy-command button.
Design / approach
Follow what the gallery now does:
- The design system supplies ONE themed, keyboard-only (
:focus-visible), never-removed focus ring globally, the shadcn convention @layer base { * { @apply border-border outline-ring/50 } } in the theme CSS.
- Light-DOM elements need NO per-element focus style (rely on the global rule).
- A shadow-DOM component is the one exception: a document rule cannot cross the shadow boundary, so it styles its own
:focus-visible in static styles.
Best-practice research (Next.js create-next-app ships NO focus rules and relies on the browser default; removing focus fails WCAG 2.4.7): the themed global outline is the right model. A refinement worth evaluating is making the outline EXPLICIT so the WIDTH is consistent too (see notes), not just the colour.
Implementation notes (for the implementing agent)
- Where to edit (per app, each has its own layout + theme):
website/: components/like-button.ts is a light-DOM <button> styled by a bare like-button button rule in app/page.ts (around L145-158) with a :hover but no :focus-visible. components/copy-cmd.ts similarly. Ensure the website's Tailwind base carries the global outline-ring/50 (or add a themed :focus-visible).
examples/blog/, docs/: audit each app's root layout / base CSS for the global focus rule and any shadow components.
packages/ui/packages/website/app/_components/preview-tabs.ts is a shadow-DOM component that currently does .tab:focus-visible { outline: none } (L54-55), an a11y regression, give it a real themed :focus-visible outline instead.
- Shadow-DOM components set their own:
button:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px } in static styles (see the gallery's reactive-meter.ts for the pattern).
- Best-practice width refinement (evaluate, do NOT rush):
outline-ring/50 themes only the COLOUR and leaves the browser-default outline WIDTH, which varies (~3px on a <button>, ~1px on an <a>). An explicit :focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px } fixes the width too. LANDMINE: a hand-written var(--color-ring) / var(--ring) in a raw @layer base rule resolved to currentColor in testing (Tailwind v4 @theme token resolution quirk), while the outline-ring/50 UTILITY resolves it fine. So the explicit form needs careful token wiring (confirm --color-ring is emitted as a runtime custom property, not @theme inline-inlined) before adopting. If it lands, apply it in the ui registry theme packages/ui/packages/registry/themes/index.css (the single source the scaffold + webjs ui add pull from) so it propagates.
- Broader consistency: while in each app, audit section-spacing rhythm, token usage, and ui class-helper usage, matching the gallery sweep.
- Invariants: shadcn API parity for
@webjsdev/ui (AGENTS invariant 5); the prose-punctuation invariant (11) for any doc edits; no vendor pins in monorepo apps.
- Docs surface: the focus convention is already documented in
.agents/skills/webjs/references/styling.md ("Focus rings"); keep it in sync if the model changes.
Acceptance criteria
Problem
The scaffold gallery was recently made internally consistent on the design system (a single section-spacing rhythm, themed tokens, and one shared focus ring; see PR #1060). The other in-repo WebJs apps (
website/,examples/blog/,docs/,packages/ui/packages/website/) were NOT swept the same way and have drifted.The concrete trigger was the focus ring. An interactive element that does NOT go through the design system's global focus rule falls back to the raw browser
outline: auto(thick, and light-coloured on a dark theme), which shows on window-refocus (:focus-visiblere-fires when the OS window regains focus). Observed on the website's live like-button and the copy-command button.Design / approach
Follow what the gallery now does:
:focus-visible), never-removed focus ring globally, the shadcn convention@layer base { * { @apply border-border outline-ring/50 } }in the theme CSS.:focus-visibleinstatic styles.Best-practice research (Next.js
create-next-appships NO focus rules and relies on the browser default; removing focus fails WCAG 2.4.7): the themed global outline is the right model. A refinement worth evaluating is making the outline EXPLICIT so the WIDTH is consistent too (see notes), not just the colour.Implementation notes (for the implementing agent)
website/:components/like-button.tsis a light-DOM<button>styled by a barelike-button buttonrule inapp/page.ts(around L145-158) with a:hoverbut no:focus-visible.components/copy-cmd.tssimilarly. Ensure the website's Tailwind base carries the globaloutline-ring/50(or add a themed:focus-visible).examples/blog/,docs/: audit each app's root layout / base CSS for the global focus rule and any shadow components.packages/ui/packages/website/app/_components/preview-tabs.tsis a shadow-DOM component that currently does.tab:focus-visible { outline: none }(L54-55), an a11y regression, give it a real themed:focus-visibleoutline instead.button:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px }instatic styles(see the gallery'sreactive-meter.tsfor the pattern).outline-ring/50themes only the COLOUR and leaves the browser-default outline WIDTH, which varies (~3px on a<button>, ~1px on an<a>). An explicit:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px }fixes the width too. LANDMINE: a hand-writtenvar(--color-ring)/var(--ring)in a raw@layer baserule resolved tocurrentColorin testing (Tailwind v4@themetoken resolution quirk), while theoutline-ring/50UTILITY resolves it fine. So the explicit form needs careful token wiring (confirm--color-ringis emitted as a runtime custom property, not@theme inline-inlined) before adopting. If it lands, apply it in the ui registry themepackages/ui/packages/registry/themes/index.css(the single source the scaffold +webjs ui addpull from) so it propagates.@webjsdev/ui(AGENTS invariant 5); the prose-punctuation invariant (11) for any doc edits; no vendor pins in monorepo apps..agents/skills/webjs/references/styling.md("Focus rings"); keep it in sync if the model changes.Acceptance criteria
website/,examples/blog/,docs/, andpackages/ui/packages/website/shows a themed--ringfocus ring on keyboard focus / window-refocus, never the raw browseroutline: auto.outline: nonewithout a themed replacement (a11y).:focus-visible.