Skip to content

feat(cards): swc-card files, styling#6515

Open
5t3ph wants to merge 6 commits into
seckles/swc-cardfrom
seckles/swc-card-files-styling
Open

feat(cards): swc-card files, styling#6515
5t3ph wants to merge 6 commits into
seckles/swc-cardfrom
seckles/swc-card-files-styling

Conversation

@5t3ph

@5t3ph 5t3ph commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Description

Implements the initial swc-card component: file/API scaffold, CardBase shared behaviors (already merged), and the shared card-template.css styling used by every card in the family.

  • swc-card scaffold: Card.ts, swc-card.ts, index.ts, card.css (placeholder for component-specific overrides)
  • Shared render function renderCardTemplate() (card-template.ts), reused by every concrete card
  • Shared card-template.css (_lit-styles/): base grid structure, size/density/visual (primary/secondary/tertiary/quiet) variants, title-as-link stretched-link + nested-target elevation, actions-slot suppression at size="xs", forced-colors override
  • CardBase (core): size/variant/density, title-as-link and selectable behaviors (click-proxy, keyboard activation, interactive-target filtering), actions-slot size propagation and conditional support, dev-mode warnings
  • Storybook stories: Playground, Overview, Anatomy, Sizes, Variants, Density, Title as link
  • Tests: 19 behavior/dev-warning tests against CardBase test-only fixtures, plus 7 per-story smoke tests

Motivation and context

Establishes the shared foundation (CardBase, renderCardTemplate(), card-template.css) that swc-user-card and swc-product-card will extend directly (sibling-inheritance, same pattern as ButtonBaseButton/ActionButton). swc-card is built first so its styling work also produces the reusable template both follow-up components depend on.

Related issue(s)

  • Jira: SWC-2361

Deferred to follow-up swc-card tickets

This PR covers structure, API, and shared styling only. Scoped out to keep the PR reviewable:

  • Collection layout — populating the collection slot (1–3 images, square aspect ratio). Tracked as its own follow-up ticket.
  • Gallery layout — the image-only layout triggered when preview is populated and title/description/actions/default/footer are all empty. Tracked as its own follow-up ticket.
  • Gallery badge/avatar via actions-slot reuse — still an open design question (Q5 in the card family plan), not implemented.
  • selectable Storybook coverage — the Selectable story is a placeholder TODO; full behaviors/accessibility story authoring lands in the documentation pass.
  • Per-unit MDX docs page and Accessibility story content — deferred to the Testing/Documentation phases for swc-card, not yet started.
  • Coordinate-based click verification — the stretched-link trick and nested-interactive-target elevation are implemented in CSS but only unit-tested at the CardBase behavior level; real browser/Playwright verification of the extended click surface is still outstanding.

Manual review test cases

  • Size and variant rendering

    1. Go to the Card Storybook page, Sizes and Variants stories
    2. Confirm each size (xs/s/m/l/xl) and variant (primary/secondary/tertiary/quiet) renders distinct, expected styling
    3. Expect no layout breakage or overlapping content at any size/variant combination
  • Actions slot at xs

    1. Set size="xs" with content in the actions slot (e.g. via the Playground story)
    2. Expect the actions content to be visually hidden
    3. Check the browser console in dev mode — expect a warning that actions aren't supported at this size
  • Title as link

    1. Go to the Title as link story
    2. Click anywhere on the card surface outside the title text itself
    3. Expect the card's linked title to activate (navigation attempt)

Accessibility testing checklist

  • Keyboard (required — document steps below)

    1. Open the Card Playground story and enable selectable via Controls
    2. Press Tab until focus reaches the card — expect a visible focus ring on the card itself (it is now a tab stop)
    3. Press Enter, then Space — expect a swc-card-click event to fire each time (check the Actions panel)
    4. Open the Title as link story and press Tab — expect focus to land on the title's anchor directly (the card itself is not an extra tab stop unless selectable is also set)
    5. With focus on the anchor, press Enter — expect normal link activation
    6. Enable both title-as-link and selectable together, Tab to the card, press Enter/Space — expect both the link activation and the swc-card-click event to fire from the same interaction
    7. With size="xs" and actions slot content present, confirm the actions content cannot receive focus via Tab
  • Screen reader (required — document steps below)

    1. With VoiceOver or NVDA, navigate to a selectable card — note that no ARIA role is set yet (deferred pending the CardView selection model; tracked in the card family plan), so it will announce as a generic focusable element rather than e.g. "button"
    2. Confirm slotted title/description/actions/footer content announces using its own native semantics — Card does not alter or relabel slotted content
    3. Confirm actions content hidden at size="xs" is not exposed to the accessibility tree (CSS display: none removes it)

Device review

  • Did it pass in Desktop?
  • Did it pass in (emulated) Mobile?
  • Did it pass in (emulated) iPad?

@5t3ph
5t3ph requested a review from a team as a code owner July 15, 2026 17:18
@5t3ph 5t3ph added Component:Card Status:Ready for review PR ready for review or re-review. 2nd gen These issues or PRs map to our 2nd generation work to modernizing infrastructure. labels Jul 15, 2026
@changeset-bot

changeset-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 05b89ca

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

}

/* Chromium :host(:has()) support */
@scope {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Explanation in this article

expect(buildUnknownTokenSuggestions('a', store)).toEqual([]);
});

it('prefers a true prefix match even when the missing suffix pushes the length delta past the fuzzy-match cutoff', () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved a small token extension issue

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.

Do the changes need to be in this PR, or could they possibly be moved out (and potentially hit main sooner)?

@github-actions

Copy link
Copy Markdown
Contributor

📚 Branch Preview Links

🔍 Gen1 Visual Regression Test Results

When a visual regression test fails (or has previously failed while working on this branch), its results can be found in the following URLs:

Deployed to Azure Blob Storage: pr-6515

If the changes are expected, update the current_golden_images_cache hash in the circleci config to accept the new images. Instructions are included in that file.
If the changes are unexpected, you can investigate the cause of the differences and update the code accordingly.

@rise-erpelding rise-erpelding self-assigned this Jul 15, 2026
@rise-erpelding
rise-erpelding self-requested a review July 15, 2026 20:38
Comment thread 2nd-gen/packages/swc/components/card/stories/card.stories.ts
Comment thread 2nd-gen/packages/swc/components/card/stories/card.stories.ts Outdated
Comment thread 2nd-gen/packages/core/components/card/Card.base.ts

@rise-erpelding rise-erpelding 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.

This looks really beautiful 🔥

--card-title-line-height: token("line-height-font-size-75");
--card-description-font-size: token("font-size-50");
--card-border-radius: token("corner-radius-500");
--card-max-inline-size: 140px;

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.

Out of curiosity, did design decide not to set max-inline-size tokens? What determines what px value we use for the max-inline size?

--card-background-color: transparent;

.card {
box-shadow: none;

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.

Minor, but is there a reason to not set a custom prop here? (--_card-box-shadow/--card-box-shadow)

───────────────────────────────────────────────────────────────────────────── */

.card {
--_card-outline-width: var(--card-outline-width, 0px);

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.

Custom props here all start with --card- or --_card-, is there a reason none of them start with --swc?

- **`<a>` as the slotted element itself — full fidelity.** `::slotted()` only matches elements *directly* assigned to a slot, never their descendants, so the [pseudo-content stretched-link trick](https://inclusive-components.design/cards/#thepseudocontenttrick) (`::slotted(a)::after`, absolutely positioned to cover the card surface) is only structurally possible in this form — it's not a browser-support question, `::slotted(a)` cannot see a nested anchor at all, by spec. Browser support for `::slotted()::after` itself has been confirmed for this project's target matrix, so where it applies, this gives real native-anchor behavior across the *entire extended surface*: middle-click, cmd/ctrl-click to open in a new tab, and right-click's "open in new tab" context menu item all work, because the browser is hit-testing the actual anchor, not a JS approximation of it. The rule itself doesn't exist yet — it lands in `card-template.css` alongside the rest of the per-component styling work, not blocked on anything.
- **Anchor nested inside a wrapper — reduced fidelity.** The stretched-link CSS trick doesn't apply. The card falls back to the JS click-proxy (below) for the extended surface: plain click/tap anywhere on the card activates the link, but middle-click, modifier-clicks, and the right-click context menu only work if the user directly targets the visible anchor text itself — the JS proxy calls `.click()`, which cannot forward modifier-key or button state the way native hit-testing does.
- **Universal fallback / primary mechanism regardless of form:** a click-proxy on the card surface finds the title's linked anchor (`renderRoot.querySelector('slot[name="title"]').assignedElements()`, checking both forms above) and calls `.click()` on it, once filtering (below) rules out clicks that landed on a different interactive target. This is what's implemented today in `Card.base.ts`, and is sufficient on its own — the CSS trick is an enhancement layered on top for the direct-anchor form, not a requirement.
- **CSS hook, independent of fidelity tier:** `[title-as-link]` (and `[selectable]`) are useful as plain attribute selectors for hover/focus-visible/cursor-pointer styling on the card surface (e.g. `:host([title-as-link]) .card:hover`), giving a consistent visual affordance that the whole card is interactive regardless of which interaction tier actually applies underneath.

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.

Just to clarify, when the migration plan says

[title-as-link] (and [selectable]) are useful as plain attribute selectors for hover/focus-visible/cursor-pointer styling on the card surface

This doesn't mean that a selectable card gets cursor: pointer on it, does it? We would only do a cursor: pointer on the title-as-link card?

Asking for a friend (Claude) who got all tripped up on this.

expect(buildUnknownTokenSuggestions('a', store)).toEqual([]);
});

it('prefers a true prefix match even when the missing suffix pushes the length delta past the fuzzy-match cutoff', () => {

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.

Do the changes need to be in this PR, or could they possibly be moved out (and potentially hit main sooner)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2nd gen These issues or PRs map to our 2nd generation work to modernizing infrastructure. Component:Card Status:Ready for review PR ready for review or re-review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants