Skip to content

feat: card component#2647

Merged
mnoleto merged 29 commits intomainfrom
feat/card-component
Mar 27, 2026
Merged

feat: card component#2647
mnoleto merged 29 commits intomainfrom
feat/card-component

Conversation

@mnoleto
Copy link
Copy Markdown
Contributor

@mnoleto mnoleto commented Mar 5, 2026

🎯 What does this PR do?

Card component

Introduces the Card compound component built on CSS Grid, following the Fondue compound component pattern.

Component structure

  • Card.Root — grid container managing hover, selected, and focus states. Renders a or overlay for click/navigation behavior.
  • Card.Banner — top image area with small / large size variants
  • Card.BannerImage — single image with cover / contain fit
  • Card.BannerIcon — centered icon placeholder for empty states
  • Card.Badges — absolutely positioned badge slot in the banner top-right
  • Card.ThumbnailImage — brand logo in the thumbnail area
  • Card.ThumbnailIcon — icon in the thumbnail area (mutually exclusive with Logo)
  • Card.Title — truncated single-line title
  • Card.Description — secondary card description
  • Card.Action / Card.ActionButton — action button aligned to the card's right edge

The component is not yet publicly exported and is marked as in_progress in Storybook.

TODO:

  • improve the styles to match the design
  • clarify some things about the design:
    • checkbox on hover
  • version with colors as banner image

🖼️ Figma
https://www.figma.com/design/vYLXkUhWGHIpEftV1zTpqI/Patterns-v1.0?node-id=2123-5667&p=f&m=dev

@mnoleto mnoleto requested a review from SamuelAlev March 5, 2026 08:27
@mnoleto mnoleto requested a review from a team as a code owner March 5, 2026 08:27
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 5, 2026

🦋 Changeset detected

Latest commit: 2687885

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@frontify/fondue-components Minor
@frontify/fondue Minor
@frontify/fondue-rte Patch

Not sure what this means? Click here to learn what changesets are.

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

@netlify
Copy link
Copy Markdown

netlify bot commented Mar 5, 2026

Deploy Preview for fondue-docs ready!

Name Link
🔨 Latest commit 20c6b42
🔍 Latest deploy log https://app.netlify.com/projects/fondue-docs/deploys/69b964a21a302900076fff1a
😎 Deploy Preview https://deploy-preview-2647.fondue-components.frontify.com
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link
Copy Markdown

netlify bot commented Mar 5, 2026

Deploy Preview for fondue-components ready!

Name Link
🔨 Latest commit 2687885
🔍 Latest deploy log https://app.netlify.com/projects/fondue-components/deploys/69c657d209770c00081a203c
😎 Deploy Preview https://deploy-preview-2647.components.fondue-components.frontify.com
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@mnoleto mnoleto changed the title Feat/card component feat: card component Mar 5, 2026
@mnoleto mnoleto force-pushed the feat/card-component branch from 9a155cf to 9cf24ca Compare March 6, 2026 08:20
mnoleto and others added 5 commits March 6, 2026 09:22
- Change default banner size from 'small' to 'large'
- Extract state-dependent styles into card-state mixin for default,
  hover, selected, and hover+selected states
- Rename grid area 'icon' to 'thumbnail' for shared logo/icon slot
- Replace hardcoded sizes with sizeToken utility
- Use transitions.transition-colors mixin for consistent animations
- Fix focus ring to use :has(> .overlay:focus-visible) instead of
  :focus-visible on non-focusable root div
- Update storybook with 280px wrapper and rename WithLargeBanner
  to WithSmallBanner to reflect new default

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update card styles to match Figma design tokens: replace hardcoded
pixel values with sizeToken/spacing variables, use inset box-shadow
for banner border, add surface-dim backgrounds, fix selection indicator
states (hidden by default, visible on hover/selected), merge shared
logo/icon styles, and add BannerFit story.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mnoleto mnoleto force-pushed the feat/card-component branch from 9cf24ca to 370da1a Compare March 6, 2026 08:22
@noahwaldner noahwaldner self-requested a review March 6, 2026 08:26
tabIndex={0}
aria-label={ariaLabel}
aria-describedby={ariaDescribedby}
aria-current={selected ? 'true' : undefined}
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.

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.

aria-selected isn't valid on link or button roles. We need an explicit role that supports it.

onKeyDown={handleKeyDown}
/>
) : (
<button
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.

This is the focusable element for the card. When it is focused by a screen reader there is no information about the content of the card at all.

Maybe we should add the `Card.Title as a sr-only text inside of it

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.

Great catch! Instead of duplicating the title as sr-only text inside the overlay (which could drift out of sync), we can use aria-labelledby to reference the Card.Title element directly. This way the overlay automatically picks up the title content as its accessible name, and if aria-label is explicitly provided it still takes precedence. The coordination between Card.Root and Card.Title will be handled via React useId()-generated id. What do you think?

@noahwaldner
Copy link
Copy Markdown
Contributor

Thank you very much for the PR! It's a great contribution!

@mnoleto mnoleto requested a review from noahwaldner March 17, 2026 09:03
Copy link
Copy Markdown
Contributor

@noahwaldner noahwaldner left a comment

Choose a reason for hiding this comment

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

The file is pretty lengthy, can we split it up into multiple comonents and hooks to improve readability?

selected?: never;
})
| {
href?: never;
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.

why do we need this type that has never for all properties?

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.

That's the non-interactive variant of the union. It ensures TypeScript errors if you pass href, onSelect, selected, aria-label, or aria-describedby to a static (non-interactive) card.

Without it, a consumer could write <Card.Root onSelect={...}> (no href) and TypeScript wouldn't complain — the union would just match the second variant (CardRootInteractiveProps & { onSelect?: never }) and silently ignore the invalid combination.

The three variants enforce:

  1. href + onSelect + selected — interactive & selectable
  2. href only — interactive, not selectable
  3. None of the above — static card, no interactive props allowed

It's a standard discriminated union pattern for mutually exclusive prop groups.

<a
className={styles.overlay}
href={href}
tabIndex={0}
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.

not needed as a <a> is focusable by default

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.

side note. does this work well with react router? ideally most of the time we'd probably want to use a Link component from react router?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We have a <Link component in Fondue to be used instead of <a (we don't have dependencies on react-router)

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.

@SamuelAlev Should I add useFondueRouter() integration for client-side routing?

I believe we should not use the Link component here. The Card already handles its own styling, disabled states, and ARIA attributes. The Card overlay is an invisible, full-surface anchor (position: absolute; inset: 0) — it has no visible text, no underline, no font styling. All the visual props Link provides (size, weight, color, underline, wrap) are irrelevant.

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.

maybe just expose an onclick and a button not an a href?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@mnoleto Sure, but keep the <a so hovering the card shows the link in the browser (also allow to drag in a new tab, cmb+click to open in new tab, ...)

@mnoleto
Copy link
Copy Markdown
Contributor Author

mnoleto commented Mar 17, 2026

The file is pretty lengthy, can we split it up into multiple comonents and hooks to improve readability?

Done!

Copy link
Copy Markdown
Contributor

@noahwaldner noahwaldner 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!

Only left comments about the Storybook structure

Image

Comment on lines +21 to +23
navigate: () => {
throw new Error('RouterProvider: `navigate` function is not implemented');
},
navigate: () => {},
// eslint-disable-next-line @eslint-react/no-unnecessary-use-prefix
useHref: () => {
throw new Error('RouterProvider: `useHref` function is not implemented');
},
useHref: (path: string) => path,
Copy link
Copy Markdown
Member

@SamuelAlev SamuelAlev Mar 26, 2026

Choose a reason for hiding this comment

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

It is on purpose throwing an error, please keep it that way and add the provider in the renderer of Storybook/tests instead 🙏

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.

Reverted the changes

Copy link
Copy Markdown
Contributor Author

@mnoleto mnoleto Mar 27, 2026

Choose a reason for hiding this comment

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

@SamuelAlev Can we still use the useHref?

@noahwaldner
Copy link
Copy Markdown
Contributor

@mnoleto can you please add the missing translations in the other languages?

@mnoleto mnoleto merged commit 2ec59ab into main Mar 27, 2026
15 of 16 checks passed
@mnoleto mnoleto deleted the feat/card-component branch March 27, 2026 12:39
@github-actions
Copy link
Copy Markdown
Contributor

Lead time: 44 days, 23 hours, 13 minutes, 43 seconds (1079.23 total hours) from first commit to close.
Review time: 22 days, 4 hours, 11 minutes, 53 seconds (532.20 total hours) from ready for review to close.

  • First commit: 10.2.2026, 14:26:07.
  • Ready for review: 5.3.2026, 09:27:57.
  • Closed: 27.3.2026, 13:39:50.

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