fix: map USWDS hero callout and dark sections onto the HDS dark palette - #166
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. |
abbybowman
left a comment
There was a problem hiding this comment.
Really nice fix @suthat , thank you for this! The root-cause read is spot on (the red is a background-color on the component, so a palette wrapper can't reach it), mapping onto the existing dark palette instead of inventing a surface is the right instinct, and the DESIGN.md contrast table is great documentation. The print reset was a great catch, and now I'm motivated to go test the rest of HDS Core as I think we likely have other components not fully playing nicely with print styles. Just have some small optional suggestions attached for your consideration.
I also have a few things I'll file separately so they don't hold up your PR, as they're outside the scope of #148 but build on your great work:
.usa-dark-backgroundis the third USWDS dark-context sibling. It usesbase-darker(Carbon gray, not the red bug), so it's less broken, but it's the one dark surface your fix doesn't bridge. Whether to bridge it too is a small design call we'll pick up in a follow-up; your:where()pattern makes it a one-line add.- Hero typography. We have an HDS Figma spec for the hero heading (you couldn't have known), so eventually I'd like to adopt
usa-heroas a first-class HDS-themed component in its own pass (heading type, callout, spacing, CTA). Your color/contrast fix is exactly the right stopgap until then and won't conflict with it. I just need to translate the Figma spec into a solid Issue with screenshots and Figma markup exports so that we can get it built.
Thanks again, this is a careful, high-quality contribution and we greatly appreciate your help! If you disagree with the edits, let me know and we can merge as is, these aren't must-dos.
| ## Live preview | ||
|
|
||
| Use the **Documentation**, **Landing Page**, and **Multi-Step Form** stories in the sidebar to see unmodified USWDS template markup rendered under the HDS Core theme. | ||
| Use the **Documentation**, **Landing Page**, and **Multi-Step Form** stories in the sidebar to see unmodified USWDS template markup rendered under the HDS Core theme. **Dark Sections** shows the hero callout and dark section on their own, which is the quickest way to check how your own dark markup will land. |
There was a problem hiding this comment.
Since we're removing the DarkSections story from the sidebar, you can link directly to it here (https://nasa.github.io/hds-core/?path=/story/guides-existing-uswds-site-dark-sections--dark-sections) so users can still get to it.
There was a problem hiding this comment.
Good point — with it out of the sidebar the sentence was pointing at something nobody could find. Linked.
One small deviation: I used the relative form, ?path=/story/guides-existing-uswds-site-dark-sections--dark-sections, rather than the full nasa.github.io URL, so it also resolves when you're running Storybook locally and matches the other cross-links in the file. Happy to switch it to the absolute URL if you'd prefer.
Done in d67c2a1.
I need to fix this in the CI pipeline so it just warns if the CSS hash changes so that a repo maintainer can run Chromatic (we have a limited free budget), but I ran it against your branch and approved the changes so this one is good to go! |
Hide the DarkSections story from the sidebar so the file reads as a pure regression test, drop the now-unnecessary a11y opt-out on the Landing Page story so a future contrast regression fails CI, and link Dark Sections from the USWDS guide since it is no longer in the sidebar. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Thanks @abbybowman — that's a genuinely useful review, and all three suggestions are in as of d67c2a1:
Story and docs changes only, so no new changeset and the API snapshot is unaffected. Both follow-ups make sense to me, and I'm glad they're separate. The print thing surprised me too — I only caught it on a whim. If you do sweep the rest of HDS Core, the pattern to look for is anything that carries a dark scheme without being a palette container, since Happy to squash before merge if that's the house style. |
|
Thanks so much again @suthat , merging now! 🚀 Filed #177 as a follow-up and am working on creating a discussion with the HDS spec for the full Hero next -- it's a little weird as it uses a gradient scrim in the background vs. a box like USWDS does, and has slots for things USWDS didn't plan for (like hero stats), so I think we may keep a "legacy USWDS markup" styling support that's what you contributed, and then ship new HDS markup to meet the full HDS hero spec (like we do for Pagination and a few others today). |
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.