Eunomia v1.1.0
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Card

Card groups related content inside a padded, bordered surface you shape yourself: a freeform box, a numbered or iconed row, an image with a caption, or a two-layer card that reveals more on hover or tap. Turn any of them into a clickable link to somewhere else.

Dependencies

eun-icon · if opening a link in a new tab
Overview API Examples Accessibility
Weekly digest

A short summary of what changed this week, sent every Monday.

When to use

Three members, one shared link behavior:

  • eun-card: a padded, bordered, optionally-shadowed surface around whatever you slot into it, arranged in a simple column with a consistent gap. It has no opinion beyond that surface: no dedicated slots for a title, a leading icon, or a description, and no forced typography on any of them. A numbered step card, an icon-and-text row, a dense list item, a stat tile, a tag list, or any other shape you need is a matter of slotting your own markup, styled with your own CSS, rather than picking a layout variant baked into the component. See Examples below for several of these shapes built this way.
  • The image card variant: an image with a title/description footer, such as a blog post preview, a product tile, or a team member card. See Alternatives below for a gallery of several images with a lightbox instead of one static image.
  • The sliding card variant: a two-layer hover/tap reveal for progressive disclosure that stays visually contained in one card-sized footprint (a stat that reveals its breakdown, a preview that reveals a call to action). See Alternatives below for when a disclosure that pushes the rest of the page down instead fits better.

Independent of what's slotted in, an appearance setting picks the surface treatment: the default look is a plain surface-colored box with a border, while a filled or raised look drops the border instead, for a muted tinted background or a drop shadow respectively. On hover, fill's tint deepens and raised's shadow grows, while a linked card also lifts and its border turns primary.

Link card

Any of the five can become a real link by giving it a destination (and, for the sliding card variant, see the "Front layer is never a link" note in the Accessibility tab, since it applies to the back layer there, not the whole card). Opening it in a new tab adds the appropriate security attributes automatically, a small arrow badge in the top-right corner, and appends "(opens in a new tab)" to the accessible name, the exact same contract as the button component's own link mode, just positioned as an absolute badge instead of an inline icon so it reads naturally against a card's free-form content instead of competing with a title or description for horizontal space.

Install & usage

Pick a framework in the toolbar above and these snippets adapt.

npm install @eunomia/elements
import "@eunomia/elements/card.js";
import "@eunomia/elements/image-card.js";
import "@eunomia/elements/sliding-card.js";
<eun-card>A bare, padded card.</eun-card>

Importing a file registers its custom element, with no further setup needed. It works with any framework, or none, since it's a standard web component.

Alternatives

You want… Prefers
Progressive disclosure that pushes the rest of the page down instead of staying contained eun-collapse
A gallery of multiple images with a lightbox eun-image-carousel

Guidance

  • Reach for eun-image-card when the shape you need is an image with a caption, and eun-sliding-card for a hover/tap reveal. Compose everything else with a plain eun-card and your own markup (see When to use and Examples above)
  • Slot a heading element (h2–h6) for any title-like content, matching the surrounding page's own outline, since eun-card enforces no heading level of its own (see the Accessibility tab)
  • Set aria-hidden="true" yourself on genuinely decorative leading content (a number, an icon), and leave it announced when it carries unique information like a step number (see the Accessibility tab)
  • Reserve target="_blank" for links that genuinely open elsewhere, since it changes the accessible name and adds the external-link badge automatically
  • Trying to make the sliding card's front layer a link. href/target only ever apply to the back layer, on purpose (see the Accessibility tab)
  • Overriding colors and spacing with inline styles instead of the --card-* CSS variables
  • Reintroducing grid/row/cell ARIA roles around a composed row layout. A card's content is purely visual, not an interactive data-grid widget (see the Accessibility tab)

Live testing

Properties

Card <eun-card>

Attributes

NameTypeDefaultDescription
aria-labelstring—An accessible label that overrides the card content. When the card also opens in a new tab, a note announcing it is appended automatically. Ignored with a slotted anchor, which takes it instead
appearance'default' | 'fill' | 'raised''default'The card's surface style. Default is a plain surface colored box with a border. Fill and raised both drop the border, fill for a muted tinted background and raised for a drop shadow. On hover or focus, fill deepens its tint and raised grows its shadow, link or not, while the primary border and the lift only apply to a linked card

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
hrefstring—The URL the card navigates to. When set, the card renders as a link instead of a plain wrapper
target'_blank' | '_self' | '_parent' | '_top'—Where to open the linked URL. Only used when href is set. Opening in a new tab automatically adds an external link badge and announces it to assistive technology, in the current locale
localestring—The locale used to resolve the "opens in a new tab" announcement. Defaults to the one set with setDefaultLocale, English otherwise
downloadstring—The filename used when the linked URL is downloaded instead of navigated to. Only used when href is set
isExternal—Whether the link, if any, opens in a new tab.
linkAriaLabel—The accessible label to apply to the rendered `<a>` : the author's own `aria-label` with "(opens in a new tab)" appended when external, unchanged otherwise. `undefined` when no `aria-label` was set at all — in that case renderNewTabHint's visually-hidden span carries the same information instead, so it isn't announced twice.
resolvedLocale—This instance's own `locale` when set, the shared default locale otherwise. Used for `Intl` formatting, so dates and numbers follow the same locale as dictionary.
dictionary—This instance's locale-resolved string dictionary — see the class doc for the resolution order.

Slots

NameDescription
(default)The card's content, arranged in a column by `--card-gap`. A native anchor as a direct child links the whole card when href is unset

Events

NameTypeDescription
eunnavigateNavigationEventFired before following href, so a client-side router can intercept the navigation. Not fired for downloads, and cancelable

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--card-paddingSets the padding of the card
--card-padding-blockSets the vertical padding, as one length applied to both sides. Ignored when --card-padding is set
--card-padding-inlineSets the horizontal padding, as one length applied to both sides. Ignored when --card-padding is set
--card-gapSets the gap between the card's own direct children
--card-border-radiusSets the corner radius of the card
--card-border-colorSets the border color, for every appearance. Fill and raised have no border by default, so it draws one when set
--card-border-widthSets the border width, for every appearance. Fill and raised have a transparent border by default, so it only shows once --card-border-color is set
--card-backgroundSets the background color, for every appearance. The fill appearance reads card-fill-background first, then this one
--card-fill-backgroundSets the background color for the fill appearance
--card-fill-hover-backgroundSets the background color on hover for the fill appearance
--card-shadowSets a box shadow for the default appearance, none by default. Replaced by a subtle elevation for the raised appearance, and unused by fill
--card-raised-hover-shadowSets the box shadow on hover for the raised appearance. Ignored when --card-hover-shadow is set
--card-hover-shadowSets the box shadow on hover or focus, for every appearance, linked or not. Defaults to the resting shadow, or a deeper one for raised
--card-overflowSets the overflow of the card, visible by default. Set it to hidden to clip a decoration or a full-bleed image to the rounded corners
--card-hover-transformSets a transform applied on hover or focus, for every appearance. Setting this always applies it, link or not. Left unset, a small lift only applies to linked cards, since a non-linked card has nothing to click
--card-hover-border-colorSets the border color on hover or focus, for every appearance, link or not. Fill and raised keep a transparent border at rest, so the color reveals with no layout shift. Defaults to the primary color for a linked card, otherwise the border stays as it is at rest, since there's nothing to click
--card-hover-backgroundSets the background color on hover or focus, for the default appearance only, unchanged by default. See card-fill-hover-background for the fill appearance
--card-hover-durationSets the duration of the hover transition
--card-transitionOverrides the card's whole hover transition, for example to animate an extra property alongside the built-in ones. Defaults to transform, outline, box shadow, background color, and border color
--card-focus-outline-colorSets the focus outline color, for linked cards only, since a non-linked card has nothing native to receive keyboard focus
--card-link-badge-offsetSets the inset of the external link badge from the card's top and right edges
--card-link-badge-sizeSets the diameter of the external link badge
--card-link-badge-backgroundSets the background color of the external link badge
--card-link-badge-colorSets the icon color of the external link badge

CSS shadow parts

NameDescription
cardThe outer box, a link once href is set, a plain container otherwise. Exposed for effects the card's CSS custom properties don't cover, such as a hover accent. It isolates its own stacking context, so a decoration placed at z-index -1 stays above its background and below its content

Image card <eun-image-card>

Attributes

NameTypeDefaultDescription
aria-labelstring—An accessible label that overrides the card content. When the card also opens in a new tab, a note announcing it is appended automatically. Ignored with a slotted anchor, which takes it instead

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
hrefstring—The URL the card navigates to. When set, the card renders as a link instead of a plain wrapper
target'_blank' | '_self' | '_parent' | '_top'—Where to open the linked URL. Only used when href is set. Opening in a new tab automatically adds an external link badge and announces it to assistive technology
localestring—The locale used to resolve the "opens in a new tab" announcement. Defaults to the one set with setDefaultLocale, English otherwise
downloadstring—The filename used when the linked URL is downloaded instead of navigated to. Only used when href is set
isExternal—Whether the link, if any, opens in a new tab.
linkAriaLabel—The accessible label to apply to the rendered `<a>` : the author's own `aria-label` with "(opens in a new tab)" appended when external, unchanged otherwise. `undefined` when no `aria-label` was set at all — in that case renderNewTabHint's visually-hidden span carries the same information instead, so it isn't announced twice.
resolvedLocale—This instance's own `locale` when set, the shared default locale otherwise. Used for `Intl` formatting, so dates and numbers follow the same locale as dictionary.
dictionary—This instance's locale-resolved string dictionary — see the class doc for the resolution order.

Slots

NameDescription
imageThe card's image, as a plain image element shown edge to edge, ignoring padding. The alt text is the author's own responsibility: describe the image, or leave it empty when it's purely decorative and the title or description already convey the same information
titleThe card's title, below the image. Use a heading element at whatever level fits the surrounding page structure. A native anchor slotted here links the whole card when href is unset. The title typography is inherited, so a composition styles its own parts freely
descriptionThe card's description, below the title. Its typography is inherited too

Events

NameTypeDescription
eunnavigateNavigationEventFired before following href, so a client-side router can intercept the navigation. Not fired for downloads, and cancelable

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--card-paddingSets the padding of the footer, holding the title and description
--card-padding-blockSets the vertical padding, as one length applied to both sides. Ignored when --card-padding is set
--card-padding-inlineSets the horizontal padding, as one length applied to both sides. Ignored when --card-padding is set
--card-gapSets the gap between the title and description
--card-border-radiusSets the corner radius of the card
--card-border-colorSets the border color of the card
--card-border-widthSets the width of the border
--card-backgroundSets the background color of the card
--card-shadowSets a box shadow, none by default
--card-hover-shadowSets the box shadow on hover or focus, linked or not. Defaults to the resting shadow
--card-overflowSets the overflow of the card, hidden by default so the image follows the rounded corners
--card-hover-transformSets a transform applied on hover or focus, for linked cards only, defaulting to a small lift
--card-hover-border-colorSets the border color on hover or focus. Defaults to the primary color for a linked card, otherwise the border stays as it is at rest, since there's nothing to click
--card-hover-backgroundSets the background color on hover or focus, linked or not, unchanged by default
--card-hover-durationSets the duration of the hover transition
--card-transitionOverrides the card's whole hover transition, for example to animate an extra property alongside the built-in ones. Defaults to transform, box shadow, background color, and border color
--card-focus-outline-colorSets the focus outline color, for linked cards only
--image-card-image-heightSets the height of the image, 180px by default. Set it to auto to size the image from its aspect ratio instead
--image-card-image-aspect-ratioSets the aspect ratio of the image, such as 4 / 3. Only applies once --image-card-image-height is auto
--image-card-title-sizeSets the font size of the title
--image-card-title-weightSets the font weight of the title
--image-card-title-colorSets the color of the title
--image-card-description-sizeSets the font size of the description
--image-card-description-weightSets the font weight of the description
--image-card-description-colorSets the color of the description
--card-link-badge-offsetSets the inset of the external link badge from the card's top and right edges
--card-link-badge-sizeSets the diameter of the external link badge
--card-link-badge-backgroundSets the background color of the external link badge
--card-link-badge-colorSets the icon color of the external link badge

CSS shadow parts

NameDescription
cardThe outer box, a link once href is set, a plain container otherwise. Exposed for effects the card's CSS custom properties don't cover, such as a hover accent
footerThe box holding the title and description, below the image

Sliding card <eun-sliding-card>

Attributes

NameTypeDefaultDescription
aria-labelstring—The accessible label for the front layer's toggle control, touch devices only, since desktop has no such control. When the back layer also opens in a new tab, a note announcing it is appended automatically
hintbooleanfalseLoops a bounce animation on touch devices while closed, hinting that the card is interactive

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
hrefstring—The URL the back layer navigates to. When set, the back layer renders as a link instead of a plain container
target'_blank' | '_self' | '_parent' | '_top'—Where to open the linked URL. Only used when href is set. Opening in a new tab automatically adds an external link badge and announces it to assistive technology
localestring—The locale used to resolve the "opens in a new tab" announcement. Defaults to the one set with setDefaultLocale, English otherwise
downloadstring—The filename used when the linked URL is downloaded instead of navigated to. Only used when href is set
isExternal—Whether the link, if any, opens in a new tab.
linkAriaLabel—The accessible label to apply to the rendered `<a>` : the author's own `aria-label` with "(opens in a new tab)" appended when external, unchanged otherwise. `undefined` when no `aria-label` was set at all — in that case renderNewTabHint's visually-hidden span carries the same information instead, so it isn't announced twice.
resolvedLocale—This instance's own `locale` when set, the shared default locale otherwise. Used for `Intl` formatting, so dates and numbers follow the same locale as dictionary.
dictionary—This instance's locale-resolved string dictionary — see the class doc for the resolution order.

Slots

NameDescription
(default)The front layer's content, always visible at rest. Never make this a link
backThe back layer's content, revealed by the slide. Avoid nesting further interactive elements inside it once href is also set on the card itself, since the whole layer already becomes a link

Events

NameTypeDescription
eunnavigateNavigationEventFired before following href, so a client-side router can intercept the navigation. Not fired for downloads, and cancelable

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--sliding-card-widthSets the width of the card
--sliding-card-heightSets the height of the card
--sliding-card-slide-offsetSets how far the front layer slides to reveal the back
--sliding-card-durationSets the duration of the slide and hint animations
--sliding-card-border-radiusSets the corner radius of both layers
--sliding-card-back-insetSets how far the back layer sits inside the front layer's own edge, on the top/left/right sides. Keeps the back layer fully hidden underneath the front layer at rest, rather than sized to match it exactly, so the front layer's rounded corners never cut through to whatever's behind the card on the page
--sliding-card-paddingSets the padding of both layers
--sliding-card-padding-blockSets the vertical padding, as one length applied to both sides. Ignored when --sliding-card-padding is set
--sliding-card-padding-inlineSets the horizontal padding, as one length applied to both sides. Ignored when --sliding-card-padding is set
--sliding-card-front-backgroundSets the background color of the front layer, and (since the host itself carries no visible box beyond what the front layer already covers) the host's own background showing through the front layer's rounded corners
--sliding-card-back-backgroundSets the background color of the back layer
--sliding-card-back-colorSets the text color of the back layer
--sliding-card-fade-colorSets the color of the bottom fade, which fades to transparent. Match your page's own background for it to blend in
--sliding-card-fade-heightSets the height of the bottom fade, and the boundary past which the slid front layer is never rendered
--sliding-card-focus-outline-colorSets the focus outline color of the front layer's toggle control, touch devices only
--card-link-badge-offsetSets the inset of the external link badge from the back layer's top and right edges
--card-link-badge-sizeSets the diameter of the external link badge
--card-link-badge-backgroundSets the background color of the external link badge
--card-link-badge-colorSets the icon color of the external link badge

CSS shadow parts

NameDescription
backThe back layer's own outer box, a link once href is set, a plain container otherwise. Exposed for effects the sliding card's CSS custom properties don't cover, such as an accent

Default

Padding, a border and a radius come built in. See Appearance below for the two border-free alternatives. The hover feedback, a border that turns primary and a small lift, only shows up once the card is a link. A plain, non-linked card stays as it is on hover, since nothing indicates it'd actually go anywhere, unless --card-hover-border-color or --card-hover-transform is set explicitly. A custom shadow at rest is the one other thing left opt-in, through --card-* custom properties. The default slot takes whatever you give it: plain text, or a structured title/description pair laid out with --card-gap between them, the same as any other element on the page.

Weekly digest

A short summary of what changed this week, sent every Monday.

A bare card, holding whatever content you slot into it, such as text, a list, or another component entirely.
<eun-card>
  <h3>Weekly digest</h3>
  <p>A short summary of what changed this week, sent every Monday.</p>
</eun-card>

<eun-card>
  A bare card, holding whatever content you slot into it — text, a list, another
  component entirely.
</eun-card>

Appearance

appearance picks the card's surface treatment, independent of whatever is slotted inside it: default (the default) is a plain surface-colored box with a border. fill and raised both keep that same border transparent instead of dropping it outright, fill for a muted tinted background, raised for a drop shadow. On hover or focus, fill's tint deepens and raised's shadow grows, and a linked card's border turns primary whatever its appearance. Keeping a transparent border rather than none at all on fill and raised means that hover color reveals with no layout shift, since the space for it was already reserved. The accompanying lift follows the same rule: it only shows up for a linked card, or once the --card-hover-transform custom property is set explicitly (see the API tab). None of the three cards below is a link, so none of them moves or turns primary on hover.

Default

Bordered. Hover to see the border turn primary.

Fill

A muted tinted background, transparent border. Hover to see the background deepen and the border turn primary.

Raised

A drop shadow, transparent border. Hover to see the shadow grow and the border turn primary.

<eun-card>Default</eun-card>
<eun-card appearance="fill">Fill</eun-card>
<eun-card appearance="raised">Raised</eun-card>

Custom hover effects

The --card-* custom properties cover common cases (a lift, a tint, a shadow), but for anything beyond that, a growing top accent, for instance, the outer box is exposed as the card shadow part, so a consumer can attach their own ::before/::after from outside without any change to the component itself. position: relative is already set on the part, so the ::before sizes itself against the card's own box regardless of appearance or what's slotted in. Hover the card below to see it in action.

Hover me

A top accent grown entirely from outside the component.

<eun-card class="hover-accent">Hover me</eun-card>
.hover-accent-demo {
  --card-hover-border-color: var(--eun-border-color);
}
.hover-accent::part(card) {
  overflow: hidden;
}
.hover-accent::part(card)::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  transform: scaleX(0);
  transform-origin: left;
  background: royalblue;
  transition: transform 0.3s ease;
}
.hover-accent::part(card):hover::before {
  transform: scaleX(1);
}

Part of the shared vocabulary, alongside every other component that reuses it.

Background decorations

A watermark, a fill rising on hover or a full-bleed image goes into the card as a regular child, positioned absolutely. The card is position: relative and isolates its own stacking context, so a decoration at z-index: -1 sits above the card's background and below its content. Set --card-overflow: hidden to clip it to the rounded corners, and --card-hover-shadow to change the shadow on hover, for every appearance.

Card title

The watermark sits behind the text, clipped by the corners.

<eun-card class="speciality">
  <span class="speciality-number" aria-hidden="true">01</span>
  <h3>Plumbing</h3>
  <p>The watermark sits behind the text, clipped by the corners.</p>
</eun-card>
.speciality {
  --card-overflow: hidden;
  --card-hover-shadow: var(--eun-shadow-l);
}
.speciality-number {
  position: absolute;
  right: -8px;
  bottom: -24px;
  z-index: -1;
  font-size: 96px;
  color: var(--eun-color-primary-alpha-10);
}

--card-overflow: hidden also clips the focus ring of a slotted anchor, drawn just outside the card. Leave it visible on a card linked that way.

Image card

An image on top, edge-to-edge, and a title/description footer below it.

Eunomia Building design systems that scale

A look at how Eunomia keeps every surface consistent.

<eun-image-card>
  <img
    slot="image"
    src="/images/eunomia-icon.svg"
    alt="Eunomia"
    style="object-fit: contain; padding: 32px;"
  />
  <span slot="title">Building design systems that scale</span>
  <p slot="description">
    A look at how Eunomia keeps every surface consistent.
  </p>
</eun-image-card>

Linking with a slotted anchor

Without href, an anchor slotted into title links the whole card. It lives in the light DOM, so it's part of the server-rendered HTML and can be your framework's own link component. The anchor is stretched over the card, which takes the same lift, border and focus ring as with href. A heading can sit inside it, and when it opens in a new tab the card shows the arrow badge and announces it.

import Link from "next/link";

<eun-image-card>
  <img slot="image" src="/products/mixer.jpg" alt="" />
  <Link slot="title" href="/products/mixer">
    <h3>Kitchen mixer</h3>
  </Link>
  <p slot="description">From 129 €</p>
</eun-image-card>;

A linked card is a single navigation target, so keep actions such as an "add to cart" button out of it: the stretched anchor covers them, and a control nested in a link is ambiguous for pointer and screen reader users alike. If one has to stay, lift it above the anchor with position: relative; z-index: 2.

Layout and typography

The footer stretches to the card's height: in a grid row, give the last element of the description margin-top: auto to align it at the bottom across cards. With neither title nor description, the footer isn't rendered, so an image-only card, such as a logo, has no empty band.

Eunomia
<eun-image-card>
  <img
    slot="image"
    src="/images/eunomia-icon.svg"
    alt="Eunomia"
    style="object-fit: contain; padding: 24px;"
  />
</eun-image-card>

The image is 180px high by default. Set --image-card-image-height: auto and --image-card-image-aspect-ratio to size it from a ratio instead.

The title and description typography is inherited rather than forced on the slotted element, so a composition slotted into either one styles its own parts with plain CSS. Tune the defaults with --image-card-title-size, -weight and -color, and their --image-card-description-* counterparts.

.product-card {
  --image-card-image-height: auto;
  --image-card-image-aspect-ratio: 4 / 3;
  --image-card-description-color: var(--eun-text-color);
}

Sliding card

Hover (or focus something inside the back layer) to reveal the back. It smoothly slides down, fading into the page background at its lower edge rather than cutting off sharply. On a touch device (try this on a phone, or narrow the viewport with the toolbar above and switch to a touch emulator), hovering isn't available, so a tap on the front layer opens it instead. Tap again, tab away, press Escape, or tap outside the card to close it.

Monthly revenue $12,480
Breakdown

Subscriptions: $9,200
One-time sales: $3,280

<eun-sliding-card>
  <div class="stat">
    <eun-icon name="monitoring" size="32"></eun-icon>
    <strong>Monthly revenue</strong>
    <span class="amount">$12,480</span>
  </div>
  <div slot="back">
    <strong>Breakdown</strong>
    <p>Subscriptions: $9,200<br />One-time sales: $3,280</p>
  </div>
</eun-sliding-card>

Touch hint

hint loops a small bounce animation on touch devices, while the card is still closed, since there's no hover affordance to discover it otherwise. No effect on desktop, where the hover reveal is already self-evident.

Tap to reveal
There it is

The bounce only loops on touch devices.

<eun-sliding-card hint>
  <div class="teaser">
    <eun-icon name="touch_app" size="32"></eun-icon>
    <strong>Tap to reveal</strong>
  </div>
  <div slot="back">
    <strong>There it is</strong>
    <p>The bounce only loops on touch devices.</p>
  </div>
</eun-sliding-card>

Composing your own layout

eun-card is a surface, not a template: it contributes the padding, border, background, and hover/focus treatment, and lays out its direct children in a column spaced by --card-gap. Everything past that, a leading icon, a title, tags, a list, a button, is your own markup styled with your own CSS, combined however a real card actually needs it. The three examples below reuse the tokens the rest of the library already exposes (--eun-space-*, --eun-font-size-*, --eun-font-weight-*, --eun-color-*) and real components (eun-icon, eun-tag, eun-button), so they stay visually consistent with everything else without the card component needing a dedicated slot or cssprop for any of them.

A leading icon or number, with a title and description

The eun-grid--lead-left grid utility already covers this exact shape: the icon or number spans both rows in its own auto-sized column, the title and description stack in the wider column next to it. Only the typography below is your own, plus a tighter row-gap to match a title sitting right above its description.

01 Create an account

Sign up with your email, it takes less than a minute.

Verify your email

We sent a confirmation link, check your inbox.

<eun-card>
  <div class="row eun-grid eun-grid--lead-left">
    <span class="lead">01</span>
    <span class="title">Create an account</span>
    <p class="description">
      Sign up with your email, it takes less than a minute.
    </p>
  </div>
</eun-card>
.row {
  row-gap: var(--eun-space-xxs, 2px);
}
.lead {
  font-size: var(--eun-font-size-l, 28px);
  font-weight: var(--eun-font-weight-l, 700);
  color: var(--eun-color-primary-500);
}
.title {
  font-weight: var(--eun-font-weight-l, 700);
}
.description {
  margin: 0;
  color: var(--eun-text-secondary);
  font-size: var(--eun-font-size-s, 14px);
}

For a denser version of this same row (a sidebar list item, a notification), there's no separate mode to reach for: tighten the card's own --card-padding/--card-gap directly, the exact same eun-card and grid utility, just given less room.

A team member card

A fuller composition: the leading-icon row above, now holding an avatar and a role, a row of skill tags underneath, and a couple of icon-only eun-buttons for contact actions, wrapped in a plain flex row of their own.

AM Alice Martin

Product Designer

Design systems Figma
<eun-card>
  <div class="row eun-grid eun-grid--lead-left">
    <span class="avatar">AM</span>
    <span class="title">Alice Martin</span>
    <p class="description">Product Designer</p>
  </div>
  <div class="tags">
    <eun-tag size="s" rounded>Design systems</eun-tag>
    <eun-tag size="s" rounded>Figma</eun-tag>
  </div>
  <div class="actions">
    <eun-button
      appearance="ghost"
      size="xs"
      icon="mail"
      aria-label="Email Alice"
    ></eun-button>
    <eun-button
      appearance="ghost"
      size="xs"
      icon="link"
      aria-label="Alice's website"
    ></eun-button>
  </div>
</eun-card>
.avatar {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--eun-color-primary-100);
  color: var(--eun-color-primary-500);
  font-weight: var(--eun-font-weight-l, 700);
}
.tags,
.actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--eun-space-xs, 4px);
}

A pricing plan card

Everything a real plan card needs, an eyebrow label, a price, a description, a feature checklist, and a full-width call to action, all just slotted content styled to taste. appearance="raised" lifts it off the page a little, fitting for a card meant to be compared side by side with others.

Pro plan
$29 /month

Everything in Free, plus priority support and custom domains.

  • Unlimited projects
  • Priority support
  • Custom domains
Upgrade
<eun-card appearance="raised">
  <span class="eyebrow">Pro plan</span>
  <div class="price">
    <span class="price-amount">$29</span>
    <span class="price-period">/month</span>
  </div>
  <p class="description">
    Everything in Free, plus priority support and custom domains.
  </p>
  <ul class="checklist">
    <li>
      <eun-icon name="check_circle" size="18"></eun-icon> Unlimited projects
    </li>
    <li>
      <eun-icon name="check_circle" size="18"></eun-icon> Priority support
    </li>
    <li><eun-icon name="check_circle" size="18"></eun-icon> Custom domains</li>
  </ul>
  <eun-button style="width: 100%; justify-content: center;">Upgrade</eun-button>
</eun-card>
.eyebrow {
  color: var(--eun-color-primary-500);
  font-size: var(--eun-font-size-s, 14px);
  font-weight: var(--eun-font-weight-l, 700);
}
.price {
  display: flex;
  align-items: baseline;
  gap: var(--eun-space-xxs, 2px);
}
.price-amount {
  font-size: var(--eun-font-size-xl, 28px);
  font-weight: var(--eun-font-weight-l, 700);
}
.price-period {
  color: var(--eun-text-secondary);
  font-size: var(--eun-font-size-s, 14px);
}
.checklist {
  display: flex;
  flex-direction: column;
  gap: var(--eun-space-xs, 4px);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--eun-font-size-s, 14px);
}
.checklist li {
  display: flex;
  align-items: center;
  gap: var(--eun-space-xs, 4px);
}

Link states

Every card in this family shares the same href/target mechanic: eun-card, eun-image-card, and (for its back layer only) eun-sliding-card.

Basic

Set href to turn the whole card into a native link. target="_blank" adds the external-link badge (top-right) and appends "(opens in a new tab)" to the accessible name. Works identically on eun-card and eun-image-card.

Read the changelog

Opens on this same page.

View source

Opens in a new tab.

Eunomia Read the full article

Opens in a new tab.

<eun-card href="https://example.com">
  <div class="row eun-grid eun-grid--lead-left">
    <eun-icon name="description" size="28" class="lead"></eun-icon>
    <span class="title">Read the changelog</span>
    <p class="description">Opens on this same page.</p>
  </div>
</eun-card>

<eun-card href="https://example.com" target="_blank">
  <div class="row eun-grid eun-grid--lead-left">
    <eun-icon name="open_in_new" size="28" class="lead"></eun-icon>
    <span class="title">View source</span>
    <p class="description">Opens in a new tab.</p>
  </div>
</eun-card>

<eun-image-card href="https://example.com" target="_blank">
  <img slot="image" src="/images/eunomia-icon.svg" alt="Eunomia" />
  <span slot="title">Read the full article</span>
  <p slot="description">Opens in a new tab.</p>
</eun-image-card>

Sliding card

href/target apply to the back layer only, never the front, which is the reveal trigger, not a destination (see the Accessibility tab).

Case study
Read the full case study

Opens in a new tab.

<eun-sliding-card href="https://example.com" target="_blank">
  <div class="teaser">
    <eun-icon name="description" size="32"></eun-icon>
    <strong>Case study</strong>
  </div>
  <div slot="back">
    <strong>Read the full case study</strong>
    <p>Opens in a new tab.</p>
  </div>
</eun-sliding-card>

SPA navigation

A linked card (eun-card, eun-image-card, or the back layer of eun-sliding-card) dispatches the same cancelable eunnavigate event as eun-link right before following href, letting an SPA router intercept it for client-side routing instead of a full page load. It's never dispatched for download links, since a file download isn't a navigation. See the SPA navigation guide for a ready-to-use router bridge per framework.

Search engines and server rendering

With href, the card's <a> only exists in the shadow root, so a crawler that doesn't run JavaScript finds no link in the server HTML. For a card leading to one of your own pages, leave href unset on eun-card and place a native anchor as a direct child instead, typically wrapping the title. The anchor is stretched over the whole card, which takes the same hover lift and focus ring as a card with href, while its accessible name stays the anchor's own text. The anchor is also what takes focus, so an aria-label goes on it. One left on eun-card is ignored in this mode, and a console warning names it.

Server-side rendering

Keep links visible to crawlers.

<eun-card>
  <a href="/guides/server-rendering/"><strong>Server-side rendering</strong></a>
  <p>Keep links visible to crawlers.</p>
</eun-card>

The anchor owns navigation, so no eunnavigate is fired for it. Keep other interactive content (buttons, other links) out of such a card, since the stretched anchor covers it. eun-image-card supports the same mode with an anchor slotted into title (see Image card above). eun-sliding-card keeps using href. See the Server-side rendering guide for the full picture.

When the anchor opens in a new tab, the card shows the same arrow badge as with href, and announces the new tab through the anchor's aria-description, in the card's locale. The anchor's text and aria-label stay as you wrote them, and an aria-description of your own is kept as is.

open-wc

Recommendations for web components.

<eun-card>
  <a href="https://open-wc.org" target="_blank" rel="noopener noreferrer">
    <strong>open-wc</strong>
  </a>
  <p>Recommendations for web components.</p>
</eun-card>

The aria-description is only a fallback: it's added once the element upgrades, and some screen readers only read a description after a pause, or leave it out of their links list. To announce the new tab everywhere, pages without JavaScript included, write the wording inside the anchor with the eun-new-tab-hint class. It becomes part of the anchor's accessible name and of the server-rendered HTML, links-ssr.css hides it visually, and the card then adds no aria-description, so the new tab is never announced twice.

import Link from "next/link";

<eun-card>
  <Link href="https://open-wc.org" target="_blank" rel="noopener noreferrer">
    <strong>open-wc</strong>
    <span className="eun-new-tab-hint"> (opens in a new tab)</span>
  </Link>
</eun-card>;

The class is hidden by links-ssr.css, so load that stylesheet, as described in the Server-side rendering guide, or the wording shows up as plain text.

Server-side rendering

The card's surface lives in its shadow root, so a server-rendered card is an inline element with no padding, border or background until it upgrades. Link card-ssr.css in the page's <head> to draw the surface from the first paint:

<link
  rel="stylesheet"
  href="node_modules/@eunomia/elements/dist/ssr/card-ssr.css"
/>

A bundler that resolves package exports can reach it as @eunomia/elements/card-ssr.css. It covers eun-card, eun-image-card and eun-sliding-card with their custom properties: every appearance, the image card's image and footer, and the sliding card's front layer at rest. Hover and focus styles, and the sliding card's back layer, arrive with the upgrade.

Custom colors

The custom properties that change colors, borders, shadows and corners, at a glance. The API tab lists every property of the component.

Variable Sets
--card-border-radius Sets the corner radius of the card
--card-border-color Sets the border color, for every appearance. Fill and raised have no border by default, so it draws one when set
--card-border-width Sets the border width, for every appearance. Fill and raised have a transparent border by default, so it only shows once --card-border-color is set
--card-background Sets the background color, for every appearance. The fill appearance reads card-fill-background first, then this one
--card-fill-background Sets the background color for the fill appearance
--card-fill-hover-background Sets the background color on hover for the fill appearance
--card-shadow Sets a box shadow for the default appearance, none by default. Replaced by a subtle elevation for the raised appearance, and unused by fill
--card-raised-hover-shadow Sets the box shadow on hover for the raised appearance
--card-hover-border-color Sets the border color on hover or focus, for every appearance, link or not. Fill and raised keep a transparent border at rest, so the color reveals with no layout shift. Defaults to the primary color for a linked card, otherwise the border stays as it is at rest, since there's nothing to click
--card-hover-background Sets the background color on hover or focus, for the default appearance only, unchanged by default. See card-fill-hover-background for the fill appearance
--card-focus-outline-color Sets the focus outline color, for linked cards only, since a non-linked card has nothing native to receive keyboard focus
--card-link-badge-background Sets the background color of the external link badge
--card-link-badge-color Sets the icon color of the external link badge
--sliding-card-border-radius Sets the corner radius of both layers
--sliding-card-front-background Sets the background color of the front layer, and (since the host itself carries no visible box beyond what the front layer already covers) the host's own background showing through the front layer's rounded corners
--sliding-card-back-background Sets the background color of the back layer
--sliding-card-back-color Sets the text color of the back layer
--sliding-card-fade-color Sets the color of the bottom fade, which fades to transparent. Match your page's own background for it to blend in
--sliding-card-focus-outline-color Sets the focus outline color of the front layer's toggle control, touch devices only
--card-hover-shadow Sets the box shadow on hover or focus, for every appearance, linked or not. Defaults to the resting shadow, or a deeper one for raised
--image-card-title-color Sets the color of the title
--image-card-description-color Sets the color of the description

Interaction

Keyboard interactions

Key Action
Tab Moves focus to a linked card (href set), or into its content
Enter Activates a linked card, or the sliding card's front toggle (touch devices only, see below)
Space Activates the sliding card's front toggle (touch devices only)
Escape Closes an open sliding card, on touch devices

A card with no href and no interactive slotted content (the common case for eun-card used as a plain display surface) is not itself focusable. There's nothing to activate, so it's correctly left out of the tab order entirely, the same way a plain <div> would be.

Touch vs. desktop interaction model

eun-sliding-card detects hover capability via matchMedia("(hover: none)"), not a fixed viewport breakpoint, so a touch-capable laptop with an attached mouse still gets the hover reveal the moment a pointing device is actually in use. On genuinely touch-only devices, the front layer becomes a real toggle control (see "Aria attributes" above). Enter/Space activates it exactly like a tap would, and it closes on Escape, on a tap/click anywhere outside the card, or when focus leaves it entirely (Tab away). The first two share the same stacking mechanism eun-modal/eun-drawer use, so a sliding-card left open behind a later-opened dialog never intercepts an Escape/outside click meant for that dialog instead.

Semantics and screen readers

Aria attributes

  • Whatever CSS you use to arrange slotted content into a row or a grid (see Examples above), no role="grid"/"row"/"cell" belongs on it: that's a purely visual layout, not an interactive data-grid widget, so none of the ARIA grid semantics apply. Adding them would be actively wrong here, since a grid role promises row/column navigation keys and a gridcell per cell, neither of which a composed card layout provides.
  • A leading icon or number you slot in is never automatically aria-hidden, since only you know whether it's purely decorative or the only place a value like a step number lives. Add aria-hidden="true" yourself on genuinely decorative content. Leave it alone (or add your own aria-label) when it conveys something unique.
  • eun-card enforces no heading level of its own. Slot in whatever heading (h2–h6) fits the surrounding page's own outline, the same way eun-image-card's title slot does. A grid of cards inside a page section already headed by an <h2> typically wants <h3> for each card's own title.
  • eun-card has no role of its own, since a card is as often a layout box as a piece of content. When a card is a self-contained item, such as a post, a review or a product, give it role="article".
  • For a linked card opening in a new tab, "(opens in a new tab)" is appended to the accessible name automatically in the card's locale, no manual wording needed, matching eun-button/eun-link. With a slotted anchor, it goes on the anchor's aria-description.
  • eun-sliding-card's front layer gets role="button"/aria-expanded/ tabindex="0" only on touch devices. On desktop, revealing the back layer is a hover affordance, not a real disclosure control, so none of those apply there. Adding them unconditionally would announce a toggle that doesn't actually exist for a mouse/trackpad user (hovering needs no activation).

Front layer is never a link, on purpose

On eun-sliding-card, href/target apply to the back layer (slot="back") only. The front layer can never become a link, even though it's the part always visible at rest. Making the front a link would defeat the whole reveal, since a click there would navigate away immediately, before the back layer (and whatever action lives on it) is ever shown. If your card needs its own destination, put that link's content in the back layer and reserve the front purely as the "there's more here" trigger.

Reduced motion

Every animation and transition in this family (the slide reveal, the open/close animation, the hint bounce, a card's own hover lift) is disabled under prefers-reduced-motion: reduce. State still changes: the back layer still becomes visible, and a card still responds to hover/focus. Only the motion used to get there is removed.

Reference links

WAI-ARIA Authoring Practices: Link Pattern
WAI-ARIA Authoring Practices: Disclosure Pattern
WCAG 2.1: Animation from Interactions