Eunomia v1.0.0-beta.2
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. All three animate on hover, a lift plus a border-color/tint/shadow change, whether or not the card is a link.

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 (h2h6) 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-labelstringAn accessible label that overrides the card content. When the card also opens in a new tab, a note announcing it is appended automatically
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. All three animate a border, tint, or shadow change on hover or focus regardless of whether the card is a link, though the accompanying lift is narrower

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
hrefstringThe 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
downloadstringThe filename used when the linked URL is downloaded instead of navigated to. Only used when href is set
isExternalWhether the link, if any, opens in a new tab.
linkAriaLabelThe 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.

Slots

NameDescription
(default)The card's content, arranged in a column by `--card-gap`

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-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 the default appearance only, since fill and raised have no border
--card-border-widthSets the width of the border, for the default appearance only, since fill and raised have no border
--card-backgroundSets the background color, for the default appearance only. See card-fill-background for the fill appearance
--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
--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 gets the border, tint, or shadow change on hover but stays in place otherwise
--card-hover-border-colorSets the border color on hover or focus, for the default appearance only. Defaults to the primary color
--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

Image card <eun-image-card>

Attributes

NameTypeDefaultDescription
aria-labelstringAn accessible label that overrides the card content. When the card also opens in a new tab, a note announcing it is appended automatically

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
hrefstringThe 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
downloadstringThe filename used when the linked URL is downloaded instead of navigated to. Only used when href is set
isExternalWhether the link, if any, opens in a new tab.
linkAriaLabelThe 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.

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
descriptionThe card's description, below the title

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-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-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, linked or not. Defaults to the primary color
--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
--image-card-title-sizeSets the font size of the title
--image-card-description-sizeSets the font size 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

Sliding card <eun-sliding-card>

Attributes

NameTypeDefaultDescription
aria-labelstringThe 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
hrefstringThe 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
downloadstringThe filename used when the linked URL is downloaded instead of navigated to. Only used when href is set
isExternalWhether the link, if any, opens in a new tab.
linkAriaLabelThe 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.

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-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, a radius, and a hover border that turns primary all come built in. See Appearance below for the two border-free alternatives. The accompanying lift only shows up once the card is a link, or once the --card-hover-transform custom property is set explicitly. A plain, non-linked card changes color on hover but doesn't move, since without either signal there's nothing indicating it'd actually go anywhere. 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 — text, a list, 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 drop the border instead, fill for a muted tinted background, raised for a drop shadow. All three react on hover, whether or not the card is a link: default's border turns primary, fill's tint deepens, raised's shadow grows. The accompanying lift is narrower in scope: 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 on hover.

Default

Bordered. Hover to see the border turn primary.

Fill

A muted tinted background, no border. Hover to see it deepen.

Raised

A drop shadow, no border. Hover to see it grow.

<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 covered in Variants, alongside every other component that reuses it.

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-fontSize-*, --eun-fontWeight-*, --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-fontSize-l, 28px);
  font-weight: var(--eun-fontWeight-l, 700);
  color: var(--eun-color-primary-500);
}
.title {
  font-weight: var(--eun-fontWeight-l, 700);
}
.description {
  margin: 0;
  color: var(--eun-text-secondary);
  font-size: var(--eun-fontSize-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-fontWeight-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-fontSize-s, 14px);
  font-weight: var(--eun-fontWeight-l, 700);
}
.price {
  display: flex;
  align-items: baseline;
  gap: var(--eun-space-xxs, 2px);
}
.price-amount {
  font-size: var(--eun-fontSize-xl, 28px);
  font-weight: var(--eun-fontWeight-l, 700);
}
.price-period {
  color: var(--eun-text-secondary);
  font-size: var(--eun-fontSize-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-fontSize-s, 14px);
}
.checklist li {
  display: flex;
  align-items: center;
  gap: var(--eun-space-xs, 4px);
}

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>

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="insights" 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>

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>

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.

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 (h2h6) 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.
  • For a linked card opening in a new tab, "(opens in a new tab)" is appended to the accessible name automatically, no manual wording needed, matching eun-button/eun-link.
  • 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.

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.

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