Blog Listing
Cards for an article index: the standard tile and the larger featured variant.

Why your design system drifts
Two artefacts maintained by hand will always diverge. Generating one from the other is the only fix that holds.
Dates and read times are strings
Neither card parses a Date or counts words. Format both upstream, where you know the reader's locale and how you want to round — "11 mins read" vs "~10 min" is an editorial choice, not something a card should decide.
Horizontal layout

Shipping dark mode from day one
Retrofitting a second theme is expensive. Building both from the same semantic layer costs almost nothing.
Featured card
The horizontal card is designed for up to 874px: a 400px image beside the text. It measures its own container, not the viewport, and when there isn't room (under about 640px), it stacks the way the vertical orientation does instead of squeezing the text.

A year of shipping from one Figma file
What we learned exporting 114 components 1:1, and the three places the approach nearly broke.
Vertical featured card

A year of shipping from one Figma file
What we learned exporting 114 components 1:1, and the three places the approach nearly broke.
BlogListingCard props
| Prop | Type | Default | Description |
|---|---|---|---|
| image / imageAlt | string | Cover image and alt text. | |
| category | string | Category label above the title. | |
| readTime | string | Pre-formatted, e.g. "11 mins read" — nothing is computed from word count. | |
| title | string | Post title. | |
| excerpt | string | Required. The standfirst under the title. | |
| author | { name: string; date: string; avatarSrc?: string; avatarAlt?: string } | Byline. date is pre-formatted and rendered after the name. | |
| layout | 'default' | 'horizontal' | 'default' | Horizontal puts the image beside the text, for list views. |
BlogListingFeaturedCard props
| Prop | Type | Default | Description |
|---|---|---|---|
| image / imageAlt | string | Cover image and alt text. | |
| badgeLabel | string | 'Featured' | The pill on the image. |
| title | string | Post title. | |
| excerpt | string | Required. The standfirst under the title. | |
| author | { name; date; avatarSrc?; avatarAlt?; initials? } | Byline. initials render when there's no avatar image. | |
| orientation | 'horizontal' | 'vertical' | 'horizontal' | Layout of image against text. |
Why it works this way
Why
- Dates and read times are strings. Neither card parses a Date or counts words, because "11 mins read" versus "~10 min" is an editorial choice made upstream, where the locale is known.
- The horizontal featured card measures its own container, not the viewport, and stacks when there isn't room (under about 640px) instead of squeezing the text.
Don't
- Don't pass a Date object or expect a read time to be computed. Format both first.
- Don't size the featured card with viewport media queries. It already responds to its container.
