Skip to content

[6.x] Cover cards: fixed fills so text and controls pass contrast over any image - #20152

Merged
brandonkelly merged 4 commits into
feature/cover-cardsfrom
feature/cover-cards-a11y
Oct 29, 2026
Merged

brandonkelly merged 4 commits into
feature/cover-cardsfrom
feature/cover-cards-a11y

Conversation

@gcamacho079

@gcamacho079 gcamacho079 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Cover cards picked their bar, backing, and scrim strength from the image's luminance, so any error in the luminance became a contrast failure. They now use fixed fills that pass over any pixel, and the luminance only picks white or black ink.

 coverAppearance(luminance, hasText)
   ink   ← top row (header) / bottom row (body), white without luminance
-  fill  ← just enough for 4.5:1 against the region's P10/P90, with floors
-          (bar 15%, scrim 40%, backing 65%)
-  controls bare when the image alone reaches 4.5:1
+  fill  ← fixed: 60% black under white ink, 52% white under black ink
+  controls always backed (65%, 80% when hovered or pressed)
  • A header gets the bar whenever it has text, including a badge.
  • Focused controls get a black and white ring.
  • Hovering or pressing a backed button strengthens its backing instead of mixing in ink.
  • The header, body, and footer don't shrink, so content that doesn't fit is clipped rather than spilling out of its fill.
  • A cover image that fails to load falls back to a normal card with its thumbnail.
packages/craftcms-ui/
├── src/components/card/
│   ├── cover.ts                      # fixed fills; luminance picks the ink
│   ├── card.edge-cases.stories.ts    # see Edge case stories
│   └── card.stories.ts               # API stories only; scrim and no-luminance stories folded in or moved
└── test/cover-fixtures.ts            # hard images and a many-attributes body, shared by stories and tests

Agent rules: what counts as a story or an edge case (craftcms-ui.md), independent test oracles (.ai/guidelines/craft.md), and contrast verification against rendered pixels (a11y.instructions.md).

Before and after

Compared with #20148 at the point this branch left it (1a60141d79). Each image follows your GitHub theme, light or dark.

Before (#20148) After (#20152)
The Laughing Cavalier cover card, before The Laughing Cavalier cover card, after
The Laughing Cavalier: its checkbox was left bare, as the image reached 4.5:1 on its own, and is now always backed. The scrim behind the title goes from 40% to the fixed 60% black.
On White II cover card, before On White II cover card, after
On White II: black ink on a light image. The scrim goes from 40% to the fixed 52% white.
The Exhibit entry type’s cover card preview, before The Exhibit entry type’s cover card preview, after
The Exhibit entry type’s card preview, with a labeled header and a badge. Light theme, over the sunset photo: the header bar goes from 28% to the fixed 52% white under black ink. Dark theme, over the night photo: it goes from 15% to the fixed 60% black. The scrim goes from 40% to 60% black in both.

Edge case stories

Edge cases now have their own folder in Storybook, beside the component, instead of mixing with the API stories or living only in tests:

Components
└── Card
    ├── Docs                      # Card.mdx: API examples only
    ├── Cover
    ├── Cover Backed Controls
    ├── Cover Active
    ├── Cover Alongside Normal Cards
    └── Edge Cases                # card.edge-cases.stories.ts
        ├── Cover Hard Images
        ├── Cover Image Not Loaded
        ├── Cover Without Luminance
        ├── Cover Broken Image
        ├── Cover Long Content
        └── Cover Many Attributes
  • Where they live: <component>.edge-cases.stories.ts, titled Components/<Name>/Edge Cases, so they nest under the component in the sidebar.
  • What goes there: hard inputs, stress content, failure states, and fallbacks for missing data. These are cases nobody configures on purpose, but reviewers need to see them.
  • Kept off the docs page: tagged !autodocs, so Card.mdx and its API examples stay focused.
  • Always published: they're in every Storybook build, so they can be reviewed from the PR preview or without a PR.
  • Shared with the tests: they use the same fixtures as the browser tests (packages/craftcms-ui/test/cover-fixtures.ts), so reviewers see exactly what the pixel tests measure. The story shows the case; the browser test verifies it.
  • Named for their mode (CoverHardImages) and sized so the whole case is visible. For example, Many Attributes uses 600px-wide cards so nothing is clipped at 4:3.

The convention is codified in .ai/rules/craftcms-ui.md. docs-references.test.ts now skips *.edge-cases.stories.ts when checking an MDX page's story references, since a component folder can now hold two stories files.

Evidence

  • Before: generated images uploaded in the CP and measured with CCA:

    • a 3:4 portrait whose measured rows are cropped out: title 2.6:1 (white ink), 3.3:1 (black ink)
    • thin stripes under the luminance's 10% threshold: title 3.4:1
    • an image that hasn't loaded: title about 3.1:1
    • the checkbox and action menu were invisible on the portraits (about 1:1)
  • After: browser and unit tests:

cover.test.ts
  text ≥ 4.5:1 over every sRGB pixel (all 16.7M), both inks                     ✓
  controls ≥ 3:1 over every pixel, resting and hovered                          ✓

card.browser.test.ts
  every line of text sits where its fill is at full strength
    (3 widths × 100/200/400% text × text spacing on/off)                        ✓
  every pixel behind the text ≥ 4.5:1 and behind the controls ≥ 3:1
    (white, black, stripes, both portraits, not loaded)                         ✓
  every pixel behind a body full of card attributes ≥ 4.5:1, none clipped       ✓
  focused controls get a black and white ring inside the card                   ✓
  a failed cover image falls back to a normal card                              ✓

with the fills dropped to the old 40%: 29 pixel tests fail                      ✗ (as expected)

Assistive technology testing

  • CCA re-measurement of the generated images in the CP

Not yet tested:

  • Keyboard: focus ring visible on every hard image
  • VoiceOver + Safari, NVDA + Firefox: cover cards announced like normal cards, image silent
  • 200% / 400% zoom and text-spacing overrides (cover cards still clip; see Known gaps)
  • Forced colors (Windows contrast themes, Chrome emulation)

Known gaps

  • Reflow: cover cards are a fixed 4:3 and clip content that doesn't fit (1.4.4, 1.4.12). Split into its own follow-up.
  • Header text without a label marker: plain text in a custom header slot doesn't get the bar.
  • Body text with its own color: e.g. inline styles in a field preview aren't guaranteed by the fills.
  • Badges: their text isn't measured here. That's left to the badge's own tests, which assumes badge fills are opaque.

Merge Danger

Door: two-way

Styles, tests, and stories only. No stored data or API changes, so it reverts cleanly.

Blast Radius: visual

Every cover card gets heavier fills: label bars go from as little as 15% to 52–60%, and light images get a stronger scrim. Header controls are always backed. Agent rules change for future UI work.

…story versus an edge case, prevent edge cases from showing up in published docs
@gcamacho079

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

…e/cover-cards-a11y

# Conflicts:
#	packages/craftcms-ui/src/components/card/Card.mdx
#	packages/craftcms-ui/src/components/card/card.styles.ts
#	packages/craftcms-ui/src/components/card/card.ts

Co-authored-by: gcamacho079 <17525979+gcamacho079@users.noreply.github.com>

Copilot AI commented Oct 29, 2026

Copy link
Copy Markdown

@copilot resolve the merge conflicts in this pull request

Resolved and pushed in cf708fd by merging feature/cover-cards, preserving the contrast fixes alongside the newer growing layout, colored borders, and JSON luminance handling. All 623 targeted tests passed, including 81 Chromium browser tests; CodeQL found no alerts.

@github-actions

Copy link
Copy Markdown

📚 Storybook previews

@craftcms/ui — open Storybook

Changed components:

resources/js — open Storybook

No changed components detected in this Storybook.

@brandonkelly
brandonkelly merged commit ad56b4c into feature/cover-cards Oct 29, 2026
20 checks passed
@brandonkelly
brandonkelly deleted the feature/cover-cards-a11y branch October 29, 2026 16:48
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