feat(adaptive-cards): add richCardTitleAsHeading styleOption to opt out of role=heading on rich card titles#5839
Open
cjennison wants to merge 4 commits into
Conversation
…ut of role=heading on rich card titles Today the title of hero/thumbnail/audio/video/animation/receipt cards is rendered with Adaptive Cards style: 'heading', which the Adaptive Cards SDK exposes as role='heading' + aria-level. This was originally requested in issue microsoft#4327 and shipped in 4.15.3. Subsequent a11y audits (e.g. for hosts where these cards appear inside a chat transcript) flag the same heading as 'Unnecessary heading level is programmatically defined for Title' under MAS 1.3.1 / WCAG 1.3.1, because card titles inside a chat are not page-level headings and break document outline tools. Reconcile the two by making the behavior configurable via styleOptions.richCardTitleAsHeading. Default is true so existing consumers (including the original microsoft#4327 reporter) keep today's behavior; consumers can pass false to drop the heading style. Adds a sibling test heroCard.noHeading.html to the existing heroCard.heading.html that asserts no .ac-textBlock[role='heading'] is rendered when richCardTitleAsHeading is false.
Contributor
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR introduces a new Adaptive Cards style option to control whether rich card titles are rendered as programmatic headings for accessibility, defaulting to the historical behavior.
Changes:
- Added
styleOptions.richCardTitleAsHeading(defaulttrue) and documented it in the style options type. - Updated rich card header rendering to optionally omit Adaptive Cards
style: 'heading'. - Added an accessibility HTML test covering the “no heading” configuration and updated the changelog.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/bundle/src/adaptiveCards/defaultStyleOptions.ts | Adds a default value for the new richCardTitleAsHeading option. |
| packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardBuilder.ts | Conditionally applies style: 'heading' to rich card titles based on the new option. |
| packages/bundle/src/adaptiveCards/AdaptiveCardsStyleOptions.ts | Documents and exposes the new style option in the public style options type. |
| tests/html2/accessibility/attachment/heroCard.noHeading.html | Adds an accessibility regression test ensuring no heading role is applied when opted out. |
| CHANGELOG.md | Announces the newly added style option. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Author
|
@microsoft-github-policy-service agree |
added 3 commits
June 7, 2026 10:48
1. heroCard.noHeading.html: scope queries to the hero card activity container instead of querying the whole document. Match the title text block by its expected text so future text blocks elsewhere on the page do not make the test flaky. 2. AdaptiveCardsStyleOptions.ts: drop the incomplete 'reverse request' bullet that had no link; keep the @see link to microsoft#4327 only. 3. AdaptiveCardBuilder.ts: add https:// prefix to the microsoft#4327 URL so tooling auto-links it.
1. AdaptiveCardBuilder.ts: drop 'as const' on the conditional 'style: heading'.
AGENTS.md says 'Avoid as'; the original code wrote 'style: heading'
without any cast because addTextBlock takes Partial<TextBlock>, and
TextBlock.style accepts string. Same here.
2. AdaptiveCardsStyleOptions.ts: tighten the doc-comment to match the
actual call graph instead of listing card types.
Cards that flow through addCommonHeaders today:
- hero (via addCommon)
- OAuth (direct)
- thumbnail no-image branch (via addCommon)
- animation/audio/video (via CommonCard -> addCommon)
Cards that DON'T (their titles use direct addTextBlock w/o style:heading):
- receipt
- thumbnail with images
- signin
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Make
role="heading"/aria-levelon hero/thumbnail/audio/video/animation/receipt card titles opt-out via a newstyleOptions.richCardTitleAsHeading(defaulttrue, preserves today's behavior).Why
Today
AdaptiveCardBuilder.addCommonHeaders()hardcodes Adaptive Cardsstyle: 'heading'on the card title TextBlock. The Adaptive Cards SDK then renders that asrole="heading"+aria-level.This was originally requested in issue #4327 (
Title in Hero Card does not havearia-levelspecified) and shipped in 4.15.3.However, downstream a11y audits — particularly for hosts where these cards appear inside a chat transcript (e.g. Microsoft Copilot Studio Test Chat) — flag the same heading as
Unnecessary heading level is programmatically defined for "Title"under MAS 1.3.1 / WCAG 1.3.1, because card titles inside a chat are not page-level headings and pollute the document outline that assistive tech relies on.The two requirements are mutually exclusive and both came from accessibility audits. The right fix is to make the host control it.
Behavior
styleOptions.richCardTitleAsHeadingtrue(default — unchanged from today)<div role="heading" aria-level="..." class="ac-textBlock">…</div>(per #4327)false<div class="ac-textBlock">…</div>(no programmatic heading)No breaking change — existing consumers keep today's output without action.
Test
Adds a sibling HTML test
__tests__/html2/accessibility/attachment/heroCard.noHeading.htmlthat mirrors the existingheroCard.heading.html, passesstyleOptions = { richCardTitleAsHeading: false }, and assertsdocument.querySelector('.ac-textBlock[role="heading"]')isnull.Changelog
Added under
[Unreleased]›Added.Notes
Required<AdaptiveCardsStyleOptions>updated in defaults.style: 'heading').addCommonHeadersis gated — that covers hero/thumbnail/audio/video/animation/receipt cards (every type that flows throughaddCommon).