Skip to content

feat(adaptive-cards): add richCardTitleAsHeading styleOption to opt out of role=heading on rich card titles#5839

Open
cjennison wants to merge 5 commits into
microsoft:mainfrom
cjennison:a11y/rich-card-title-as-heading-option
Open

feat(adaptive-cards): add richCardTitleAsHeading styleOption to opt out of role=heading on rich card titles#5839
cjennison wants to merge 5 commits into
microsoft:mainfrom
cjennison:a11y/rich-card-title-as-heading-option

Conversation

@cjennison
Copy link
Copy Markdown

Summary

Make role="heading" / aria-level on hero/thumbnail/audio/video/animation/receipt card titles opt-out via a new styleOptions.richCardTitleAsHeading (default true, preserves today's behavior).

Why

Today AdaptiveCardBuilder.addCommonHeaders() hardcodes Adaptive Cards style: 'heading' on the card title TextBlock. The Adaptive Cards SDK then renders that as role="heading" + aria-level.

This was originally requested in issue #4327 (Title in Hero Card does not have aria-level specified) 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.richCardTitleAsHeading Title TextBlock renders as
true (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.html that mirrors the existing heroCard.heading.html, passes styleOptions = { richCardTitleAsHeading: false }, and asserts document.querySelector('.ac-textBlock[role="heading"]') is null.

Changelog

Added under [Unreleased]Added.

Notes

  • Followed AGENTS.md style: sorted property bag, no one-use intermediates, Required<AdaptiveCardsStyleOptions> updated in defaults.
  • Subtitle and text TextBlocks are intentionally left untouched (they never had style: 'heading').
  • Only addCommonHeaders is gated — that covers hero/thumbnail/audio/video/animation/receipt cards (every type that flows through addCommon).

…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.
Copilot AI review requested due to automatic review settings June 6, 2026 02:04
Copy link
Copy Markdown
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

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 (default true) 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.

Comment thread __tests__/html2/accessibility/attachment/heroCard.noHeading.html Outdated
Comment thread packages/bundle/src/adaptiveCards/AdaptiveCardsStyleOptions.ts Outdated
Comment thread packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardBuilder.ts Outdated
@cjennison
Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Christopher Jennison 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
expect(titleTextBlock).toBeTruthy();

expect(titleTextBlock.getAttribute('role')).toBe(null);
expect(heroCardActivity.querySelector('.ac-textBlock[role="heading"]')).toBe(null);
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.

Suggestion: add both test one with visible styling for [role="heading"], and this one. Add snapshots for both so the change can be visually inspected.

Copy link
Copy Markdown
Collaborator

@OEvgeny OEvgeny left a comment

Choose a reason for hiding this comment

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

Looks good, waiting for the tests update

Comment thread CHANGELOG.md

### Added

- Added `styleOptions.richCardTitleAsHeading` (default `true`) to opt out of `style: 'heading'` on rich card titles, by [@cjennison](https://github.com/cjennison). Resolves the conflict with [#4327](https://github.com/microsoft/BotFramework-WebChat/issues/4327) for hosts where card titles are not navigational headings.
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.

Follow our format.

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.

4 participants