fix: map USWDS hero callout and dark sections onto the HDS dark palette - #166
fix: map USWDS hero callout and dark sections onto the HDS dark palette#166suthat wants to merge 1 commit into
Conversation
USWDS fills .usa-hero__callout and .usa-section--dark with primary-darker and colors the headings inside them accent-cool. Under the HDS theme that renders a NASA-red block with cyan headings, a red call-to-action sitting on the red hero box, and HDS components inside the section still reading white-palette values — .usa-link came out at 1.8:1 and outline button labels at 2.1:1 against the red. A palette wrapper cannot reach any of it, because the red is a background-color on the component itself. Both surfaces now carry the full dark scheme (palette 4, Carbon 90), so headings, text, links, buttons, and focus rings resolve against the surface they actually sit on. The hero's --alt eyebrow takes the muted token so the two-tone heading survives; that one is flagged for creative director review in DESIGN.md. Selectors stay inside :where(), so an adopter who wants a different surface still wins with a palette class on the same element. Print styles reset both surfaces as well, including USWDS's white <p> and <a> inside .usa-section--dark, which the palette reset only reached by inheritance and lost. Adds a Dark Sections story whose six-palette axe check fails on the pre-fix CSS, so the regression cannot come back unnoticed. Refs nasa#148 Co-authored-by: Cursor <cursoragent@cursor.com>
|
Heads up on the red X: Chromatic is failing on That's the fork-PR secrets rule — GitHub doesn't expose Every other check is green, including the ones that would actually catch a mistake here: public API contract, CSS hash comparison, the test suite, bundle size, token drift, and all the linters. One thing worth knowing before someone kicks Chromatic off: this PR will produce real visual changes that need accepting as new baselines. That's expected, not a regression — the whole point is that the hero callout and Happy to rebase or re-run anything if it helps. |
What this PR does
I picked this up from #148, and the first thing I did was build the CSS and drop the untouched USWDS landing page template on top of it, because I wanted to see the thing with my own eyes before deciding anything. The issue undersells it a little. The hero callout is a slab of dark red with a cyan headline sitting on it, there's a red button on the red box, and further down the graphic list sits on the same red with more cyan. Nothing else in HDS looks remotely like that. If I were a team migrating a site over from USWDS, this is one of the first screens I'd see, and I'd probably conclude the theme was broken and go file a ticket — which is roughly what happened here.
The cause is short. USWDS paints both of its dark layout contexts —
.usa-hero__calloutand.usa-section--dark— withcolor("primary-darker"), and colors the headings inside them withcolor("accent-cool"). HDS maps the primary family to NASA Red, and accent-cool is a family HDS never themes at all, so the red and the cyan both come straight through untouched.This PR maps both surfaces onto palette 4 (dark, Carbon 90), so everything sitting on them — headings, body copy, links, buttons, focus rings — resolves against the surface it's actually on.
Closes #148
The part I got wrong first
My instinct was the lazy one: this is exactly what the palette system is for, so just tell adopters to wrap the section in
.hds-palette-darkand close the issue with a docs change. I'm glad I tried it before writing it down, because it doesn't work, and the reason is the whole reason this fix has to live in the framework instead of the guide.The red isn't a palette custom property. It's a plain
background-coloron the component itself, so no ancestor wrapper can reach it. And when I wrapped it anyway,.hds-palette-whitemade things actively worse — the headings gave up their cyan and became Carbon Black on the red block. So the advice we'd have been handing adopters was "add this class and it gets harder to read."Then I stopped eyeballing it and measured what was actually shipping. A couple of these were worse than I expected:
primary-darker#8b0a03#17171baccent-cool#00bde3, 4.38:1--hds-palette-headingwhite, 17.9:1--alteyebrow--hds-palette-mutedCarbon 30, 9.1:1.usa-linkin a dark section.usa-button--outlinelabel.usa-buttonon the hero callout#d83933on#8b0a03— 2.1:1 against its own container#d83933on Carbon 90, 3.9:1The three in bold fail WCAG 2.1 AA. The link one in particular bothered me: 1.8:1 is not "a bit low," it's a link you cannot see. That happens because an HDS component dropped inside a USWDS dark section is still reading white-palette values — nothing ever told it the ground underneath it had changed color.
What I chose, and why
I deliberately didn't invent anything. HDS already has a designed dark surface, and it already knows how to relate every token to it, so I mapped the two USWDS contexts onto that rather than picking a new dark red or a new neutral. It means no new colors enter the system, and adopters get correct behaviour on markup they already shipped without going back and adding classes to it.
I kept the selectors inside
:where()so they carry zero specificity. If a team wants a different surface there, they drop.hds-palette-blueon the same element and win, no!importantneeded. That's the same contract as the USWDS surface bridges that already live directly above this block inbase/_palettes.scss— I tried hard to make this read like it belongs next to them rather than like something bolted on afterwards.Two things I chose not to touch, in case it looks like an oversight. USWDS pins
<p>and<a>inside.usa-section--darkto white on the elements themselves; I left those alone, because white is what the dark scheme resolves to anyway, and the blue palette — the other surface someone would plausibly swap in on a section marked dark — is also dark with white body text. And.usa-section--lightis untouched: USWDS paints itbase-lightest, which the HDS theme already maps to white, so it agrees with the default palette without help.The bit I'm least sure about
The USWDS hero heading is two-tone: a small
--alteyebrow above the statement line. Once the cyan is gone, painting both with the heading color flattens it into one undifferentiated block of white, and the design intent of the component quietly disappears.I gave the eyebrow the muted token HDS already uses for overlines and metadata (Carbon 30, 9.1:1 on Carbon 90). It reads well and it keeps the hierarchy, but I want to be honest that this is a design call and not a contrast fix, so I've written it up in
docs/DESIGN.mdunder Creative Director Review rather than burying it. If design would rather the eyebrow stay full white, or take a different treatment entirely, it's one declaration to change and I'll happily change it.Something I nearly shipped a bug on
Themed dark surfaces created a print problem I didn't see coming and only caught because I ran the page through print emulation on a whim.
Browsers drop background colors when printing, so white text on those sections would have printed as a blank page. The existing reset in
base/_print.scssonly covers palette containers, and these two aren't palette containers — they carry the dark scheme without being one — so they're now listed there too. USWDS pins those<p>and<a>colors on the elements themselves, where the reset only reached them by inheritance and lost, so those get an explicit black.Type of change
I went back and forth on patch vs minor. It's a fix in intent, but it visibly changes markup adopters already have on their pages, and the semver rubric treats that as a visible change rather than a silent correction — so, minor. Push back if you read the rubric differently.
Checklist
npm run format:fixandnpm run lint:scss:fixpassnpm run lint:jsandnpm run lint:mdpassnpm test)I don't like ticking boxes without saying what's behind them, so:
The new
PaletteA11ystory renders the hero and the dark section inside all six palettes and runs axe across the lot. Viewports I checked by hand at 320, 640 and 1024 — the callout goes full-bleed on mobile and the graphic list collapses to one column, and both keep the surface and stay readable.The a11y one I want to flag specifically. A test that passes tells you nothing until you've watched it fail, so I reverted the SCSS and re-ran the suite to make sure the new story actually catches this bug rather than just going green next to it. On the pre-fix CSS it fails on
color-contrastfor.usa-hero__heading(2.14:1) and.usa-button--outline(2.14:1). Reapplied the fix, clean. That's the bit that makes me comfortable this regression can't quietly come back.Docs went to
docs/DESIGN.md(rationale, before/after, the design question flagged),docs/ARCHITECTURE.md(why layer order handles this without specificity tricks, plus the print note),stories/guides/USWDS.mdx(adopters no longer need to hand-wrap these two, but other dark contexts like.usa-dark-backgroundstill need it), andAGENTS.mdso nobody — me included, six months from now — "simplifies" this away without knowing what it was for.Public API and changesets
npm run update:api-snapshotand reviewed the diffnpx changeset) with appropriate bump levelThe snapshot picks up three selectors:
.usa-hero__callout,.usa-hero__heading--alt,.usa-section--dark. No new Sass symbols, so the public Sass surface is unchanged.Visual review
Storybook: Guides → Existing USWDS Site → Dark Sections is the focused case, and Landing Page shows both surfaces in the full USWDS template if you want to see them in context.
Notes for reviewers
The eyebrow color is the one open question; everything else is built from tokens that already exist, so it should be uncontroversial, but I'd rather you tell me than assume.
Worth saying out loud: this will visibly change any site currently shipping a USWDS hero callout or
.usa-section--darkunder HDS. Red block becomes Carbon 90. That's the point of the issue, but somebody will notice it in a release, so the changeset says so plainly.On the
status:needs-design-reviewlabel — I read that as "the color decision needs a designer to sign off," not "don't touch this yet," so I've put forward a specific answer assembled entirely from existing tokens rather than leaving it open. If that was the wrong read, tell me and I'll pull it back to a proposal. If design wants a different surface, it's one block inbase/_palettes.scss.Scope I deliberately left alone: other USWDS dark contexts like
.usa-dark-backgroundand custom section modifiers. Those still need an adopter-applied palette class. I kept this to the two surfaces named in the issue rather than expanding it on my own initiative — happy to follow up in a separate PR if you'd like the same treatment there.