You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A work list for the Tag family: what people already expect from a semantic element or a combination of elements, and the CSS (TagBrush.css) or JavaScript (TagAction) that would deliver it. Only gaps are listed; anything in the guides today is left out. Written November 10, 2026 against TagBrush.css 1.2.0 + unreleased, TagAction 0.2.0 + unreleased.
For the implementer
Every repo's CLAUDE.md and the family Spec.md still apply: no classes as API, hooks in the order native → ARIA → data-tag*, pages work without JavaScript, one PR per pattern (TagBrush) with CHANGELOG, README, llms.txt and demo in the same change.
Budgets: TagBrush.css is 11.0 of 15 KB gzipped, TagAction 9.0 of 15 KB, the TagBrush guide index 3,038 of 4,000 tokens. Each item below must fit in one guide line.
Each CSS pattern needs a false-match test (nearby markup it must leave alone). Each behavior needs mouse, keyboard, dynamic-insertion and no-JS tests.
New attributes and commands must be added to TagCheck's vocabulary (unknown-attribute, unknown-command) in the same round.
Before writing an item marked mwg, retrieve that guide: npx -y modern-web-guidance@latest retrieve <id>. Follow its fallback rules; do not assume browser support from memory.
Decided November 10, 2026: section 2 ships as TagBrush.css 2.0 (the new look is the default, no opt-in variable), and the sticky header hook is <header data-layout="sticky">.
1. Defects (fix first)
Found by rendering TagKit's templates with the local builds.
#
Where
What happens
Fix
D1
TagAction password.js
The Show button wraps the input in a role="group" div, so a help <small> after the field is no longer next to it. In signup.html "At least 10 characters." lands inline beside the next label. The validation message placement ("a <small> right after the field") likely breaks the same way.
Move a following <small> (and the message element) so it follows the group, or look past the group when placing. Test with the sign-up template.
D2
TagBrush stat tiles
<del>−0.4%</del> in a <dl> tile is struck through on a red highlight, so a drop reads as a deleted value. <ins> is a green highlight.
In dl > div, ins and del are plain colored text (success, error), no line, no background, with ▲/▼ before them (content: "▲ " / ""). Elsewhere unchanged.
D3
Tables
Only cells holding a lone <data> align right. In the blog template "Customers" (42, 118) is left-aligned next to a right-aligned "Revenue", and no numeric header lines up with its column.
TagAction's sort already knows which columns are numeric: mark those th and td (data-tag="num"), and style them in TagAction's own sheet: text-align: end; font-variant-numeric: tabular-nums. An author's align/text-align wins.
D4
TagAction theme.js
The saved theme is applied when the deferred script runs, after first paint on long pages: a flash of the other scheme.
Apply data-theme at module top level (before start()), and say in the guide that a page with a theme switch loads the script without defer.
D5
Page footer columns
Columns bunch at the start and leave half the row empty.
The leading paragraph takes the free space; the <nav> columns sit at the end.
2. The look: reading as designed, not generated (TagBrush.css 2.0)
ROADMAP.md chunk 23 left this open; it is now decided as the 2.0 default. The rendered templates confirm it: the landing page shows most of the marks people now read as machine-made.
Mark of a generated page
Where TagBrush.css does it today
Proposed default
Every block in the same tinted, bordered, rounded box
Feature cards, plans, testimonials, stat tiles, callouts, code, quotes all share one frame
Frame by role. A box means "one discrete thing you can act on or compare": plans, clickable cards, stat tiles. Feature cards (no link, no picture) lose the box: icon, heading, text, separated by space. A box has a fill or a border, never both. Shadow only on overlays and the featured card.
Icon on a tinted rounded tile
svg first in a card
Plain icon at heading size in the text color.
Uppercase, letter-spaced label in the brand color over each heading
hgroup > p before a heading
Sentence case, muted text color, normal spacing.
Soft glow of the brand color behind the hero
--hero-background radial gradient
none by default; the variable stays.
Blue-tinted "slate" neutrals and a navy dark mode
Text, surface, border and dark background all take the primary's hue (chroma × 0.14–0.15)
Near-neutral: cap the chroma carried into neutrals at about 0.012, so the brand color appears only where it means something.
Brand color on everything that can take it
Header and sidebar nav links, table links, eyebrows are all primary blue
The primary color is for the main action, links inside prose, and focus. Nav links are text-colored (current page: full-contrast and heavier; others muted or with a hover fill).
Quote and callout look alike
blockquote and article aside are both a filled box with a start rule
A quote is text: start rule, no fill, normal text color. A callout keeps the fill.
Even, tight rhythm
About 2rem between landing sections; h1 sits on its subtitle
In main[data-layout="full"], sections get generous block padding (clamp(3rem, 8vw, 6rem)). Space separates sections; boxes don't. More room between a hero h1 and its paragraph.
One type size contrast
h1 tops out at 2.5rem
A hero h1 (in body > header hgroup) grows to about 3.5rem with tighter tracking. App and article h1 unchanged.
The price is body text
"$12 per month" in a plan card (roadmap follow-up)
In a card, a paragraph that starts with <strong> or <data> followed by <small> is a price: large figure, small muted period.
What makes a page feel hand-built is less the palette than completeness: every state is considered (hover, pressed, selected, busy, empty, error, disabled), numbers line up, nothing shifts, and the keyboard works. Sections 3 and 4 are that work, and they matter more than this table.
Not proposed: a new default brand color (any default becomes the tell; presets with font pairings in the theme builder do more), scroll reveals, hover lift on every card, gradient text.
3. Expectations by element
Priority: P1 people notice its absence; P2 familiar from good sites; P3 polish. "CSS" is TagBrush.css, "JS" is TagAction. Without JavaScript each falls back to the plain element.
P1 CSS: pressed = filled with a tint. TagAction's own Show/Hide and theme buttons gain a visible state.
<button data-tone="error">
A destructive action is red, most of all in a confirm dialog
P1 CSS: extend data-tone to buttons (solid and .outline). Update TagKit's confirm-dialog snippets.
aria-keyshortcuts="/" on a field or button
"/" focuses search; a shortcut shown is a shortcut that works
P2 JS: one key listener. A key (or Control+K, Meta+K) focuses a field or clicks a button. Ignored while typing in a field, unless it has a modifier.
<button popovertarget> + [popover], TagAction dropdown list
The menu stays on screen: flips up or aligns to the other edge near a viewport edge
P2 CSS (and TagAction's sheet for [data-tag="dropdown"]): anchor positioning with fallbacks. mwgviewport-aware-dropdowns
[popover="hint"] with interestfor
A tooltip on hover and focus
P3 CSS only: style it small and inverted. No JS fallback (tooltips stay out of scope). mwginterest-triggered-tooltips
Forms
Markup
People expect
Work
label + :required field
To see which fields are required before submitting
P1 CSS: label:has(+ :required)::after, label:has(> :required): content: " *" / "" in the error color. Only in a form that also has optional fields (form:has(:optional:not(button, [type="hidden"], [type="submit"]))).
<textarea> in a form
Ctrl+Enter or ⌘+Enter sends it
P1 JS: form.requestSubmit(), so validation runs. mwgime-safe-enter-submit
role="status", role="alert"
A "Saved" or "Couldn't save" banner looks like one
P1 CSS: a banner with a tone (status info, alert error; data-tone overrides). Server-rendered or swapped in by htmx. No auto-dismiss.
command="--clone" rows
Every added row can be removed
P1 JS: command="--remove" with commandfor naming the row (ids in clones are already renumbered). Focus moves to the previous row or the Add button. Written hidden in the template's first, static row.
input[type="file"][accept^="image"]
A thumbnail of the chosen picture
P2 JS: object-URL <img> in the existing file list; revoke on change.
search input[type="search"]
A magnifier in the field
P3 CSS: background icon from --color-text-muted.
Submit with invalid fields
Focus goes to the first problem and it scrolls into view
Exists (form.js). Add a test that the field is not under a sticky header (see scroll-padding, below).
Tables and lists
Markup
People expect
Work
Filter box + table or list, nothing matches
"No matches" instead of an empty table, and a count for screen readers
P1 JS: one role="status" line after the target ("No matches for 'x'", otherwise visually hidden "12 of 40 shown").
Filter box with a name
The filter survives reload and can be shared
P2 JS: start from ?name= in the URL; history.replaceState as it changes.
Filter box, matches
The matching text is highlighted
P2 JS + TagAction sheet: CSS Custom Highlight API, no DOM changes. mwghighlight-text-ranges
thead th > input[type="checkbox"], a checkbox first in each row
Select all, a mixed state, Shift-click for a range, selected rows tinted
It eases open; the marker is a chevron, not the browser's triangle
P1 CSS: chevron marker at the end of the summary in an accordion, at the start elsewhere; height transition where supported, instant otherwise and under reduced motion. mwganimate-to-intrinsic-sizes
<details>, printing
A printed FAQ includes the answers
P2 JS: open all on beforeprint, restore on afterprint. CSS print block: hide nav, button, search, [data-tag] controls; avoid breaks inside cards.
Tabs
Find-in-page reaches text in a hidden panel; a link to a panel's id selects its tab
P2 JS: hide panels with hidden="until-found", select on beforematch, and select from the #fragment (as details.js does).
Modal <dialog>
The page behind doesn't scroll, and nothing shifts sideways when it opens
P1 CSS: html:has(dialog:modal) { overflow: hidden } with scrollbar-gutter: stable on the root.
Modal <dialog> on a phone
A sheet from the bottom edge, full width
P3 CSS: below about 30rem, dock to the bottom with top corners rounded; slide instead of scale.
Image viewer with several pictures
Arrow keys and buttons step through the set
P3 JS: pictures in the same <figure> or section form a set. Add a TagKit gallery snippet (asked for in eval round 1).
Page and navigation
Markup
People expect
Work
<header data-layout="sticky"> (opt-in)
A top bar that stays, gains a hairline or shadow once stuck, and never covers a heading you jumped to
P2 CSS: position: sticky, scroll-padding-top on the root via :has(), stuck styling where supported. mwgstate-aware-sticky-headers
a[target="_blank"] in prose
A sign that it opens elsewhere
P2 CSS: ::after { content: " ↗" / " (opens in a new tab)" }. Not on buttons, nav, cards, or image links.
Sidebar aside nav on a phone
It collapses instead of pushing content down
Check what happens today at 360px; if it stacks above main, document the popover phone-menu pattern for sidebars and add it to the dashboard template.
4. Not doing
Sticky table headers (a scrolling <figure> gives sticky nothing to hold to), drag-to-reorder, multi-step form wizards, toasts that time out, infinite scroll: components or accessibility traps.
Automatic link prefetching: a prefetched GET /logout signs people out.
Animated row reordering on sort, scroll reveals, parallax: motion as decoration.
A tooltip fallback in JavaScript, and anything that needs a class.
5. Order of work
D1–D5.
P1 CSS in TagBrush (chevron, pressed, danger button, required mark, status and alert banners, details marker and easing, dialog scroll lock), one PR each.
P1 JS in TagAction (Ctrl+Enter, --remove, filter empty state), then TagCheck vocabulary and TagKit snippets that use them.
Section 2 as TagBrush.css 2.0, one release with before/after screenshots of all six templates in light and dark.
P2, then P3.
Eval round 2 with requests that need the new patterns (a selectable admin table, a destructive confirm, a form with repeatable removable rows, a saved-status banner).