Skip to content

fix: map USWDS hero callout and dark sections onto the HDS dark palette - #166

Merged
abbybowman merged 3 commits into
nasa:mainfrom
suthat:fix/uswds-hero-dark-section-theming
Aug 4, 2026
Merged

fix: map USWDS hero callout and dark sections onto the HDS dark palette#166
abbybowman merged 3 commits into
nasa:mainfrom
suthat:fix/uswds-hero-dark-section-theming

Conversation

@suthat

@suthat suthat commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

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__callout and .usa-section--dark — with color("primary-darker"), and colors the headings inside them with color("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-dark and 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-color on the component itself, so no ancestor wrapper can reach it. And when I wrapped it anyway, .hds-palette-white made 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:

Element Before After
Surface USWDS primary-darker #8b0a03 Carbon 90 #17171b
Headings on those surfaces accent-cool #00bde3, 4.38:1 --hds-palette-heading white, 17.9:1
Hero --alt eyebrow White — fine on its own, but it's the line that gets flattened once the cyan goes --hds-palette-muted Carbon 30, 9.1:1
.usa-link in a dark section Carbon 90 on red, 1.8:1 White, 17.9:1
.usa-button--outline label Carbon Black on red, 2.1:1 White, 17.9:1
.usa-button on the hero callout #d83933 on #8b0a032.1:1 against its own container #d83933 on Carbon 90, 3.9:1

The 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-blue on the same element and win, no !important needed. That's the same contract as the USWDS surface bridges that already live directly above this block in base/_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--dark to 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--light is untouched: USWDS paints it base-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 --alt eyebrow 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.md under 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.scss only 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

  • Bug fix (patch)
  • New feature or component (minor)
  • Breaking change (see Public API section below)
  • Documentation only
  • Tooling or CI (no effect on published output)

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:fix and npm run lint:scss:fix pass
  • npm run lint:js and npm run lint:md pass
  • Tested across all 6 palettes in Storybook
  • Tested across mobile, tablet, and desktop viewports
  • Automated a11y checks pass (npm test)
  • Storybook documentation updated (if component changed)

I don't like ticking boxes without saying what's behind them, so:

The new PaletteA11y story 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-contrast for .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-background still need it), and AGENTS.md so nobody — me included, six months from now — "simplifies" this away without knowing what it was for.

Public API and changesets

  • Ran npm run update:api-snapshot and reviewed the diff
  • Changeset added (npx changeset) with appropriate bump level
  • Bump level matches the semver rubric

The 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--dark under 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-review label — 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 in base/_palettes.scss.

Scope I deliberately left alone: other USWDS dark contexts like .usa-dark-background and 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.

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>
@suthat

suthat commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Heads up on the red X: Chromatic is failing on Missing project token, not on a visual diff. It dies 23 seconds in, before it builds anything.

That's the fork-PR secrets rule — GitHub doesn't expose secrets.CHROMATIC_PROJECT_TOKEN to pull_request runs from a fork, so projectToken arrives empty and the CLI bails immediately. Same thing happened on #160 with the identical error, and Chromatic ran fine once it went through from inside the repo. Nothing I can do from this side.

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 .usa-section--dark stop being NASA Red and become Carbon 90. Expect diffs on the Landing Page story and on the new Dark Sections story.

Happy to rebase or re-run anything if it helps.

@abbybowman
abbybowman self-requested a review August 1, 2026 13:44

@abbybowman abbybowman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-background is the third USWDS dark-context sibling. It uses base-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-hero as 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.

Comment thread stories/guides/USWDSDarkSections.stories.js
Comment thread stories/guides/USWDSLandingPage.stories.js
Comment thread stories/guides/USWDS.mdx Outdated
## 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@abbybowman

Copy link
Copy Markdown
Contributor

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 .usa-section--dark stop being NASA Red and become Carbon 90. Expect diffs on the Landing Page story and on the new Dark Sections story.

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>
@suthat

suthat commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @abbybowman — that's a genuinely useful review, and all three suggestions are in as of d67c2a1:

  • DarkSections hidden with tags: ['!dev'], so the file is a pure regression test. PaletteA11y is unchanged.
  • The a11y: { test: 'todo' } opt-out removed from the Landing Page story. Full suite re-run locally with it enforced — 242 tests, all green.
  • Dark Sections now linked from the USWDS guide, since it's no longer in the sidebar. I used the relative ?path= form so it works in local Storybook too; easy to change if you'd rather have the absolute URL.

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. .usa-dark-background I left out on purpose because it's a design call rather than a contrast failure, so it's better made deliberately than tacked onto this — and yes, it's one line inside the same :where() block whenever you decide. On the hero: good to know there's a Figma spec, and I'd rather this stay a stopgap than pre-empt it. If it's useful when you write that issue, the eyebrow treatment here (muted / Carbon 30) is the one thing I'd expect the real spec to overrule, and it's a single declaration.

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 base/_print.scss only walks the palette selectors.

Happy to squash before merge if that's the house style.

@suthat
suthat requested a review from abbybowman August 4, 2026 14:48
@abbybowman

Copy link
Copy Markdown
Contributor

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).

@abbybowman
abbybowman merged commit e72ce60 into nasa:main Aug 4, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Bare USWDS hero and dark sections render off-brand (red blocks + cyan headings) under HDS

2 participants