Skip to content

fix: keep icons and state indicators visible in forced-colors mode - #259

Open
suthat wants to merge 3 commits into
nasa:mainfrom
suthat:fix/forced-colors-icons-229
Open

suthat wants to merge 3 commits into
nasa:mainfrom
suthat:fix/forced-colors-icons-229

Conversation

@suthat

@suthat suthat commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Fixes the #229 items that don't have a PR yet: accordion, site alert, error message, checkbox, text field and side nav. While testing I found two more problems of the same kind and fixed them too: a checked checkbox looks unchecked on light themes, and the radio's circle disappears on focus. Radio selection (#232), the external link arrow (#234) and table sort (#252) are left to their own PRs. This branch merges cleanly with all three.

Refs #229

The cause is the same everywhere. In forced colors the browser repaints any background-color that isn't a system color as Canvas (keeping only its alpha) and drops box-shadow. An icon drawn with mask-image and a palette color ends up Canvas on Canvas. A system color set in author CSS is kept, and the mask still paints (checked in Chromium 153), so each fix is a system color inside @media (forced-colors: active). forced-color-adjust: none isn't needed.

Component What broke Fix
Accordion Chevron repainted as Canvas. The USWDS +/- sits on ::before, which the focus ring also uses, so it vanished on focus Chevron in ButtonText; USWDS +/- hidden in forced colors
Site alert Icon repainted as Canvas CanvasText
Error message (checkbox, radio, input, select) Icon repainted as Canvas CanvasText
Checkbox and radio, focused The focus rule's outline: none removed the USWDS high-contrast outline, which is the only thing drawing the box Keep $border-high-contrast on focus
Checkbox, checked (not in the audit) Fill repainted as Canvas, leaving a white check on Canvas. Invisible on light themes; Aquatic is dark, so the audit missed it Highlight fill with the check cut out (mask-composite: exclude), like a native checkbox. Disabled + checked uses GrayText
Text input, textarea, select outline: none !important left a 1px → 2px border change as the only focus cue Reset limited to not (forced-colors: active), as pagination does since #230, so the global focus outline paints
Side nav and in-page nav Current-page bar repainted as Canvas over the CanvasText line, so it reads as a gap Highlight, set in the shared hds-vertical-nav-links mixin

Please confirm these calls:

  • I used Highlight rather than SelectedItem. In Chromium's forced-colors emulation, SelectedItem stays #b3d7ff on both the light and dark palettes (1.4:1 on white), while Highlight follows the palette. This may matter for fix(form): preserve radio selection in forced colors #232, which uses SelectedItem.
  • Errors get no accent hue. Icon, text and border carry the error in system colors, as the issue suggests.
  • In forced colors the accordion now shows the HDS chevron where USWDS shows +/-. The reason is in the SCSS. I didn't add an MDX note because DOCUMENTATION.md keeps USWDS notes to markup differences.
  • I didn't change the side-nav hover underline. The dashed underline still paints, so switching to a solid one is a design call for a separate change.

Type of change

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

How to review

Turn on forced colors (a Windows contrast theme, or DevTools > Rendering > Emulate CSS media feature forced-colors: active) and check one light and one dark theme:

  • Accordion: Tab to a heading. The chevron stays and the outline shows.
  • Site Alert, All Variants: icons show.
  • Checkbox, Default / States / With Error: checked boxes are filled with a visible check, the error icon shows, and the box keeps its outline when focused.
  • Radio Button: the circle keeps its outline when focused.
  • Text Input and Select: focus shows a solid outline outside the border.
  • Side Navigation and In-Page Navigation: the current item has a solid marker.

With forced colors off, nothing should change.

Before requesting review

  • npm run lint, npm run format, and npm test pass locally

If your change affects how anything renders (styles, tokens, story markup, icons, or fonts):

  • Checked across all 6 palettes in Storybook
  • Checked across mobile, tablet, and desktop viewports
  • Did a manual a11y pass (keyboard, focus, screen-reader labels)

If your change ships in the package (styles, tokens, or src/assets/**):

  • Added a changeset (npx changeset) at the semver rubric level
  • Ran npm run check:api-snapshot; no drift

Notes for reviewers

Testing:

  • New test, scripts/forced-colors/__tests__/icons-and-states.test.js. It renders the markup against the compiled Sass in Chromium with forced colors on, light and dark, and checks that each indicator paints something other than Canvas or draws an outline. It also checks that the media query matched and the HDS CSS loaded. All 26 cases fail on main for the expected reason and pass on this branch.
  • Palettes: swept 29 stories (every Palette A11y story, plus All Variants, States and With Error), 260 indicators in total. On main all 260 paint as Canvas on both forced palettes; on this branch none do.
  • Viewports: 18 stories for the touched components at 375, 768 and 1280 px, each with forced colors off, light and dark. With forced colors off, all 54 full-page screenshots are byte-identical to main. With forced colors on, no indicator paints as Canvas (0 of 132, against 132 of 132 on main), nothing overflows horizontally, and I looked through the mobile screenshots for both palettes.
  • Keyboard: tabbed through every control in those 18 stories, in all three modes and at all three widths (435 stops). Tab order is the same as on main, and every stop shows a visible focus change in forced colors. The compiled CSS diff only adds forced-colors blocks and moves the input outline: none !important under not (forced-colors: active), so focus outside forced colors is unchanged.
  • Screen-reader labels: the accessibility tree (ariaSnapshot) of all 18 stories matches main in every mode and width (162 of 162). The change is CSS only.
  • npm test (272 passed, including axe on every story), lint, format, check:api-snapshot, check:tokens, check:uswds, check:uswds-core.
  • All on macOS with Chromium's emulation. I haven't tried a native Windows contrast theme, Firefox, or a live screen reader.

Docs: COMPONENTS.md now has the forced-colors step in the mask-icon recipe and in the manual checks. ARCHITECTURE.md mentions the forced-colors outline on text inputs.

Found but left out, probably worth separate issues:

  • Outside forced colors, a text input or select in the error state shows no focus indicator at all: .usa-input--error:focus-visible keeps the 1px red border and the outline is reset. That's 2.4.7 and it's the same on main. This PR only changes it in forced colors, where the outline now paints.
  • Plain .usa-alert icons vanish too. USWDS paints them black through a mask, so the bug is upstream.
  • The blockquote quote icon vanishes the same way. It's decorative, so I left it.
  • A disabled, checked radio paints as Canvas because HDS's disabled rule overrides USWDS's GrayText. This fits with fix(form): preserve radio selection in forced colors #232.

Forced colors repaints every author background-color that is not a
system color as Canvas, so icons drawn with mask-image +
background-color, state fills, and marker bars paint Canvas on Canvas
and disappear. System colors are kept, so each now gets one inside
@media (forced-colors: active):

- Accordion: chevron in ButtonText; hide the USWDS +/-, which shares
  ::before with the focus ring and vanished on focus
- Site alert and .usa-error-message icons: CanvasText
- Checked checkbox: Highlight fill with the check cut out, so it shows
  on light and dark themes; disabled + checked uses GrayText
- Checkbox and radio: keep the USWDS high-contrast box outline on focus
- Text input, textarea, select: limit `outline: none !important` to
  normal mode so the global focus outline paints
- Side and in-page nav current-page bar: Highlight

Default rendering is unchanged. Adds a Chromium forced-colors
regression test, and adds the forced-colors step to the mask-icon
recipe in COMPONENTS.md.

Refs nasa#229
abbybowman added a commit to nulluid/hds-core that referenced this pull request Oct 6, 2026
The arrow paints in CanvasText without opting the pseudo-element out
of forced colors; verified on a native Windows contrast theme after
@suthat flagged it in nasa#259. Reword the comment and drop the matching
test assertion.
@abbybowman
abbybowman self-requested a review October 7, 2026 15:33

This branch has not been deployed

No deployments
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]: Icons and form-control state indicators disappear in forced-colors mode (radio selection not perceivable)

1 participant