Skip to content

fix(a11y): color contrast, skip link, focus rings, and aria attributes#421

Open
gabitoesmiapodo wants to merge 3 commits intotest/demo-smokefrom
fix/a11y
Open

fix(a11y): color contrast, skip link, focus rings, and aria attributes#421
gabitoesmiapodo wants to merge 3 commits intotest/demo-smokefrom
fix/a11y

Conversation

@gabitoesmiapodo
Copy link
Collaborator

Summary

  • Fix warning color contrast: #cc0 fails WCAG AA; #996600 (light) / #e6b800 (dark)
  • Fix danger/ok semantic tokens: add proper dark-mode variants (#ff6666, #66ee66)
  • Add skip-to-main-content link for keyboard navigation
  • Add aria-invalid to BigNumberInput on range validation failure
  • Replace outline:none with _focusVisible focus ring on CopyButton and ExternalLink
  • Add aria-label="Close" to icon-only close buttons in Modal and TokenInput
  • Add aria-label="View on explorer" to icon-only ExternalLink in Hash

Test plan

  • pnpm test passes
  • Verify focus ring visible on CopyButton and ExternalLink with keyboard
  • Verify skip link appears on Tab from address bar

Copilot AI review requested due to automatic review settings March 23, 2026 21:45
@vercel
Copy link

vercel bot commented Mar 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
components.dappbooster Ready Ready Preview, Comment Mar 24, 2026 0:04am
demo.dappbooster Ready Ready Preview Mar 24, 2026 0:04am
docs.dappbooster Ready Ready Preview, Comment Mar 24, 2026 0:04am

Request Review

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves accessibility across the app by addressing color-contrast issues, adding keyboard navigation support (skip link + focus rings), and improving screen-reader semantics with ARIA attributes.

Changes:

  • Update semantic color tokens (warning/ok/danger) to improve WCAG contrast in light/dark modes.
  • Add a skip-to-main-content link and restore visible focus styling for icon-only controls.
  • Add/extend ARIA attributes for validation and icon-only buttons/links.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/routes/__root.tsx Adds a skip-to-main-content link and a main-content anchor target.
src/components/ui/provider.tsx Updates semantic token colors for contrast-compliant warning/ok/danger states.
src/components/sharedComponents/ui/Modal/index.tsx Adds aria-label to icon-only close button.
src/components/sharedComponents/ui/ExternalLink/index.tsx Replaces outline="none" with _focusVisible focus ring styling.
src/components/sharedComponents/ui/CopyButton/index.tsx Replaces outline="none" with _focusVisible focus ring styling.
src/components/sharedComponents/TokenInput/index.tsx Adds aria-label to icon-only close button in the token dialog.
src/components/sharedComponents/Hash.tsx Adds aria-label to icon-only explorer external link.
src/components/sharedComponents/BigNumberInput.tsx Introduces aria-invalid wiring based on range validation failures.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

outlineOffset: '2px',
borderRadius: '2px',
}}
transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate"
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

transition value looks malformed: the last token is missing the closing } (color {durations.moderate). This will likely result in an invalid CSS transition string and the transition being ignored. Close the token (and consider reusing the same corrected string wherever this pattern appears).

Suggested change
transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate"
transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate}"

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed: added the closing } to the last token.

borderRadius: '2px',
}}
textDecoration="none"
transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate"
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

transition value looks malformed: the last token is missing the closing } (color {durations.moderate). This can invalidate the whole transition declaration. Close the token so Chakra can resolve the duration correctly.

Suggested change
transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate"
transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate}"

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed: same as above.

Comment on lines 138 to 152
const inputProps = {
disabled,
onChange: updateValue,
placeholder,
type: 'text',
}

return renderInput ? (
renderInput({ ...inputProps, inputRef })
) : (
<chakra.input
aria-invalid={hasError || undefined}
{...inputProps}
ref={inputRef}
/>
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

aria-invalid is only applied in the default <chakra.input> branch. When renderInput is provided (which appears to be the common path), the rendered input won’t receive aria-invalid on range validation failures. Consider adding aria-invalid to inputProps before calling renderInput, so both branches get it consistently.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed: aria-invalid is now part of inputProps before the renderInput branch, so both paths receive it.

as="main"
direction="column"
flexGrow="1"
id="main-content"
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

Skip link target is a <main> element, which is not focusable by default. Following href="#main-content" may scroll but leave keyboard focus on the skip link, reducing its usefulness. Add tabIndex={-1} (or otherwise ensure the target can receive focus) so activation moves focus into main content.

Suggested change
id="main-content"
id="main-content"
tabIndex={-1}

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed: added tabIndex={-1} to the <Flex as="main"> element.

Comment on lines 127 to 133
}] and value is: ${value}`
console.warn(message)
onError?.({ value, message })
setHasError(true)
} else {
setHasError(false)
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

hasError is only updated inside the min/max range check. Other early-return paths in updateValue (e.g. empty string, parseUnits failure, decimals-length guard) don’t clear hasError, so aria-invalid can remain true after the user fixes/clears the input. Consider resetting hasError (and optionally clearing onError) on those early returns to keep validity state accurate.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed: setHasError(false) is now called on the empty-string early return path.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/components/sharedComponents/BigNumberInput.tsx:74

  • hasError is only updated inside updateValue, so if the parent updates value programmatically (e.g. reset to 0 / set max) after an out-of-range entry, aria-invalid can remain stuck true even though the new value is valid. Consider deriving/clearing hasError in an effect when value/min/max change, or resetting it whenever value is externally synchronized.
  const [hasError, setHasError] = useState(false)

  // update inputValue when value changes
  useEffect(() => {
    const current = inputRef.current

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 72 to 73
// update inputValue when value changes
useEffect(() => {
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

In the useEffect that syncs the DOM input value, the parseUnits(...) call inside the effect can throw if the input currently contains an unparseable intermediate string (possible because updateValue returns early on parse errors). Since this effect runs on external value updates, an external update while the user has invalid text could crash rendering; wrap the parseUnits call in a try/catch or guard the string before parsing.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed: wrapped parseUnits in a try/catch; on failure a sentinel value of BigInt(-1) forces the DOM value to be overwritten, ensuring the external value prop always wins when the current input is unparseable.

…butes

- Fix warning color contrast: #cc0 fails WCAG AA; use #996600 (light) / #e6b800 (dark)
- Fix danger/ok colors: add proper light/dark variants (#ff6666, #66ee66) for dark mode contrast
- Add skip-to-main-content link in root layout for keyboard navigation
- Add aria-invalid to BigNumberInput default input on range validation failure
- Replace outline:none with _focusVisible focus ring on CopyButton and ExternalLink
- Add aria-label="Close" to icon-only close buttons in Modal and TokenInput
- Add aria-label="View on explorer" to icon-only ExternalLink in Hash component
- Fix malformed transition string in CopyButton and ExternalLink (missing
  closing brace on last token)
- Move aria-invalid into inputProps so it applies to both the default input
  and any custom renderInput renderer
- Clear hasError state on empty-string input path (was only cleared on
  valid range path, leaving aria-invalid=true after user clears the field)
- Add tabIndex={-1} to #main-content so skip link activation moves
  keyboard focus into the main region
An intermediate/unparseable input string while the user is typing could
cause the useEffect that syncs the DOM value on external prop changes to
throw. Wrap parseUnits in a try/catch and use a sentinel value to force
the DOM overwrite when the current string is not parseable.
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.

2 participants