Skip to content

Ensure 4.5:1 contrast for syntax-highlighted code and keep code blocks when messages are already in the store - #5893

Merged
Eugene (OEvgeny) merged 7 commits into
mainfrom
OEvgeny/fix-code-block-contrast
Oct 18, 2026
Merged

Eugene (OEvgeny) merged 7 commits into
mainfrom
OEvgeny/fix-code-block-contrast

Conversation

@OEvgeny

@OEvgeny Eugene (OEvgeny) commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

Comments in syntax-highlighted code have 4.36:1 color contrast in the Fluent theme, below the 4.5:1 required by WCAG 1.4.3 (axe color-contrast, serious).

Changelog Entry

Fixed

  • Fixed syntax-highlighted code having less than 4.5:1 color contrast: comments in the light code block theme (github-light-default) were 4.36:1 in the Fluent theme and now use the muted text color of the theme, #656d76 (5.03:1), in PR #5893, by @OEvgeny
  • Fixed code blocks becoming empty when Web Chat is rendered with messages already in the store, such as again with the same store or from a saved state: code is shown without highlighting until the syntax highlighter is loaded, in PR #5893, by @OEvgeny

Description

Web Chat paints the code block background itself. The Fluent theme uses #fafafa (light) and #141414 (dark). The GitHub code themes were designed for #ffffff and #0d1117. On #fafafa, comments of github-light-default (#6e7781) are 4.36:1 by the WCAG formula, which axe rounds down to 4.35:1. The "View code" dialog of side-by-side/codeblock.navigation.html hit this, so that check was skipped.

The light code block theme passed to Shiki now replaces the comment color with colorReplacements: { '#6e7781': '#656d76' }. #656d76 is the muted text color of the same theme (GitHub Primer's fg.muted), with about the same hue: 5.03:1 on #fafafa and 5.25:1 on #ffffff. The dark code block theme is unchanged.

The same Shiki integration also left code blocks empty when Web Chat was rendered with messages already in the store: rendered again with the same store, or with a store created from a saved state. These messages rendered before the new highlighter had loaded. CodeHighlighterComposer then called an undefined highlighter, and its error handler threw on highlightCode.name. The code block had already moved its code out, so it stayed empty. While no highlighter is set, CodeHighlighterComposer now uses the one above it, which is the plain-text highlighter by default.

Design

  • Only the failing color is replaced, statically, with Shiki's colorReplacements on the theme. Only comments fail: every other foreground of the two themes that JavaScript, Python, and TypeScript tokens use is at least 4.83:1 in the light theme (#8250df on #fafafa) and 5.99:1 in the dark theme (#8b949e on #141414). The other rules below 4.5:1 have background-like foregrounds (markup.ignored, markup.untracked, carriage-return), which the bundled grammars do not produce. A Shiki update with new theme colors, or a new language, could bring a failing color; the accessibility check in the new tests catches it for the sampled tokens.
  • #656d76 is the color the theme itself uses for muted text, so comments stay in its palette. It is checked against the Fluent background, which is darker than the default theme's #ffffff. It also leaves a margin for hosts that set --codeBlockBackground to a slightly darker gray, such as Fluent's colorNeutralBackground3 #f5f5f5 (4.81:1). Hosts are responsible for the contrast of their own background, as before.
  • The default theme also gets #656d76 for comments. It already passed at 4.55:1, but there is one highlighter for both themes. Existing snapshots are unchanged: the difference is under the image snapshot threshold. The accessibility check in the new tests catches a regression.
  • The new test opens the "View code" dialog because axe cannot measure text in the transcript: the transcript focus overlays (pointer-events: none) cover it, and axe reports it as "incomplete" instead of failing.
  • The code block theme is the codeBlockTheme style option and does not follow the color scheme, and the code block background does not follow the Fluent UI theme. So a dark Web Chat can show either code block theme on the same backgrounds as a light one. Verified combinations (lowest token contrast, measured in the transcript and in the "View code" dialog):
    • Default theme, light code theme (#ffffff): 5.05:1 (#8250df; comments 5.25:1). Dark code theme (#0d1117): 6.15:1. The default theme has no dark variant, and prefers-color-scheme: dark does not change either background.
    • Fluent theme, light or no Fluent UI theme: light code theme (#fafafa) 4.83:1 (#8250df; comments 5.03:1), dark code theme (#141414) 5.99:1.
    • Fluent theme in a dark Fluent UI theme (webDarkTheme): light code theme 4.83:1, dark code theme 5.99:1. The same in the Copilot variant and with a custom brand ramp.
  • markdown/vnext/markdownStreaming.html keeps its skips: without them, it fails only for the level-one heading ([Programmatic Access - BOT Framework - Web Chat > Enter Keyword]: Multiple H1 headings are defined for the controls like 'Accessibility', 'Adaptive card', 'Animation card' and etc. #4721), not for color contrast.

Follow-ups, not in this PR:

  • In the dark side-by-side pages, the FAQ link in the "View code" dialog is 2.81:1 (#5661d4, the test palette's brand[100], on #292929). The skip is now limited to the dark theme.
  • The accessibility check cannot measure the color contrast of text in the transcript, as described above.

Specific Changes

  • bundle/src/codeHighlighter/shiki.ts: the light code block theme (github-light-default) replaces the comment color #6e7781 with #656d76

  • component/.../createCodeHighlighterComposer.tsx: highlightCode is optional. Without it, the upstream highlighter is used

  • test/page-object/.../createDirectLineEmulator.js: the subscribe function is no longer async. Its promise was called as a teardown function when Web Chat unmounted, which logged TypeError: n is not a function

  • Added __tests__/html2/markdown/codeBlock/contrast.html (default and Fluent themes, light and dark code themes, and the Fluent theme in a dark Fluent UI theme with either code theme): opens the "View code" dialog for JavaScript, TypeScript, and Python with comments, strings, keywords, and other tokens, with the default accessibility check. Fails on main in the Fluent theme, light and dark: insufficient color contrast of 4.35 (foreground color: #6e7781, background color: #fafafa)

  • Added __tests__/html2/markdown/codeBlock/remount.html: renders Web Chat again with the same store and expects the Python code block to be highlighted. Fails on main with the code block empty and TypeError: Cannot read properties of undefined (reading 'name')

  • __tests__/html2/side-by-side/index.html: the "View code" dialog step is checked again in the light theme, and the TODO now only mentions the FAQ link in the dark theme

  • I have added tests and executed them locally

  • I have updated CHANGELOG.md

  • I have updated documentation

Review Checklist

This section is for contributors to review your work.

  • Accessibility reviewed (tab order, content readability, alt text, color contrast)
  • Browser and platform compatibilities reviewed
  • CSS styles reviewed (minimal rules, no z-index)
  • Documents reviewed (docs, samples, live demo)
  • Internationalization reviewed (strings, unit formatting)
  • package.json and package-lock.json reviewed
  • Security reviewed (no data URIs, check for nonce leak)
  • Tests reviewed (coverage, legitimacy)

@OEvgeny

Copy link
Copy Markdown
Collaborator Author

TODO: dark variant verification for both light and dark codeblock themes

@OEvgeny

Eugene (OEvgeny) commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator Author

Verified: the code block theme is independent of the color scheme, so a dark Fluent UI theme (incl. Copilot) gives the same lowest contrast as light, 4.60:1 with the light code theme and 5.99:1 with the dark one; contrast.fluent.dark.html and contrast.code-block-dark.fluent.dark.html now run the accessibility check on both.

@OEvgeny
Eugene (OEvgeny) marked this pull request as ready for review October 17, 2026 15:16
Copilot AI balanced review requested due to automatic review settings October 17, 2026 15:16

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread packages/bundle/src/codeHighlighter/shiki.ts

@compulim William Wong (compulim) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't know where these strings are, but they are in the screenshots.

Typescript -> TypeScript
Javascript -> JavaScript

Eugene Olonov added 5 commits October 17, 2026 18:35
Comments of github-light-default (#6e7781) are 4.36:1 on the #fafafa code
block background of the Fluent theme. Token colors below 4.5:1 against the
Fluent code block backgrounds are now mixed with black or white, keeping
their hue, through Shiki's colorReplacements.

The "View code" step of side-by-side is checked again in the light theme;
its dark theme still fails for the FAQ link.
ShikiComposer has no highlighter until Shiki loads. Activities already in
the store render before that, CodeHighlighterComposer called an undefined
highlighter and threw in its error handler, and the code block stayed
empty. Without a highlighter, CodeHighlighterComposer now uses the
upstream one.

createDirectLineEmulator subscribed with an async function, whose promise
was called as a teardown function when Web Chat unmounted.
Also describe the remount fix as rendering with messages already in the
store, which includes a new store created from a saved state.
Add contrast.fluent.dark.html and contrast.code-block-dark.fluent.dark.html:
the Fluent theme in a FluentProvider with webDarkTheme, with the light and the
dark code block theme. The code block theme is a style option and does not
follow the color scheme, so both combinations are reachable.
…ing every color

Instead of computing replacements for every token color below 4.5:1 at
runtime (ensureContrast), the light code block theme now replaces only the
color that fails: comments in github-light-default (#6e7781) are 4.36:1 on
the Fluent theme code block background (#fafafa). They now use the muted
text color of the same theme, #656d76 (GitHub Primer fg.muted): 5.03:1 on
#fafafa and 5.25:1 on #ffffff.

Every other color of JavaScript, Python, and TypeScript tokens is at least
4.83:1 (light theme, #8250df on #fafafa) and 5.99:1 (dark theme, #8b949e on
#141414), so the dark theme is unchanged.
@OEvgeny

Eugene (OEvgeny) commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

William Wong (@compulim) the language comes from payload in lowercase string and we capitalize the first letter via CSS, if I recall correctly.

@OEvgeny
Eugene (OEvgeny) enabled auto-merge (squash) October 18, 2026 06:26
@OEvgeny

Copy link
Copy Markdown
Collaborator Author

William Wong (@compulim) thanks for unblocking the PR and I'll make a follow-up mapping name to language title. We can make it localizable too.

@OEvgeny
Eugene (OEvgeny) merged commit 0041a38 into main Oct 18, 2026
32 checks passed
@OEvgeny
Eugene (OEvgeny) deleted the OEvgeny/fix-code-block-contrast branch October 18, 2026 06:32
Eugene (OEvgeny) added a commit that referenced this pull request Oct 18, 2026
…seconds (#5895)

Waits for the Python code to be highlighted, then advances the clock by 5 seconds.
Needs #5893: without it, a code block with a language that is rendered before Shiki has loaded stays empty.

Co-authored-by: Eugene Olonov <eugeneolonov@microsoft.com>
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.

3 participants