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
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.
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/card.js";
</script>
<eun-card>A bare, padded card.</eun-card>
npm install @eunomia/elements
import "@eunomia/elements/card.js";
function Info() {
return <eun-card>A bare, padded card.</eun-card>;
}
npm install @eunomia/elements
"use client";
import "@eunomia/elements/card.js";
export function Info() {
return <eun-card>A bare, padded card.</eun-card>;
}
npm install @eunomia/elements
<script setup>
import "@eunomia/elements/card.js";
</script>
<template>
<eun-card>A bare, padded card.</eun-card>
</template>
npm install @eunomia/elements
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@eunomia/elements/card.js";
@Component({
selector: "app-info",
template: `<eun-card>A bare, padded card.</eun-card>`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class InfoComponent {}
Alternatives
eun-collapseeun-image-carouselGuidance
- Reach for
eun-image-cardwhen the shape you need is an image with a caption, andeun-sliding-cardfor a hover/tap reveal. Compose everything else with a plaineun-cardand 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, sinceeun-cardenforces 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/targetonly 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
| Name | Type | Default | Description |
|---|---|---|---|
| aria-label | string | — | An 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
Properties
JS-only — no matching HTML attribute, set these from a script or a template binding.
| Name | Type | Default | Description |
|---|---|---|---|
| href | string | — | 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 |
| download | string | — | 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. |
Slots
| Name | Description |
|---|---|
| (default) | The card's content, arranged in a column by `--card-gap` |
Events
| Name | Type | Description |
|---|---|---|
| eunnavigate | NavigationEvent | Fired 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
CSS custom properties
| Name | Description |
|---|---|
| --card-padding | Sets the padding of the card |
| --card-gap | Sets the gap between the card's own direct children |
| --card-border-radius | Sets the corner radius of the card |
| --card-border-color | Sets the border color, for the default appearance only, since fill and raised have no border |
| --card-border-width | Sets the width of the border, for the default appearance only, since fill and raised have no border |
| --card-background | Sets the background color, for the default appearance only. See card-fill-background for the fill appearance |
| --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-transform | Sets 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-color | Sets the border color on hover or focus, for the default appearance only. Defaults to the primary color |
| --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-hover-duration | Sets the duration of the hover transition |
| --card-transition | Overrides 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-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-offset | Sets the inset of the external link badge from the card's top and right edges |
| --card-link-badge-size | Sets the diameter of the external link badge |
| --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 |
CSS shadow parts
| Name | Description |
|---|---|
| card | The 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
| Name | Type | Default | Description |
|---|---|---|---|
| aria-label | string | — | An 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
Properties
JS-only — no matching HTML attribute, set these from a script or a template binding.
| Name | Type | Default | Description |
|---|---|---|---|
| href | string | — | 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 |
| download | string | — | 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. |
Slots
| Name | Description |
|---|---|
| image | The 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 |
| title | The card's title, below the image. Use a heading element at whatever level fits the surrounding page structure |
| description | The card's description, below the title |
Events
| Name | Type | Description |
|---|---|---|
| eunnavigate | NavigationEvent | Fired 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
CSS custom properties
| Name | Description |
|---|---|
| --card-padding | Sets the padding of the footer, holding the title and description |
| --card-gap | Sets the gap between the title and description |
| --card-border-radius | Sets the corner radius of the card |
| --card-border-color | Sets the border color of the card |
| --card-border-width | Sets the width of the border |
| --card-background | Sets the background color of the card |
| --card-shadow | Sets a box shadow, none by default |
| --card-hover-transform | Sets a transform applied on hover or focus, for linked cards only, defaulting to a small lift |
| --card-hover-border-color | Sets the border color on hover or focus, linked or not. Defaults to the primary color |
| --card-hover-background | Sets the background color on hover or focus, linked or not, unchanged by default |
| --card-hover-duration | Sets the duration of the hover transition |
| --card-transition | Overrides 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-color | Sets the focus outline color, for linked cards only |
| --image-card-image-height | Sets the height of the image |
| --image-card-title-size | Sets the font size of the title |
| --image-card-description-size | Sets the font size of the description |
| --card-link-badge-offset | Sets the inset of the external link badge from the card's top and right edges |
| --card-link-badge-size | Sets the diameter of the external link badge |
| --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 |
CSS shadow parts
| Name | Description |
|---|---|
| card | The 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
| Name | Type | Default | Description |
|---|---|---|---|
| aria-label | string | — | 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 |
| hint | boolean | false | Loops a bounce animation on touch devices while closed, hinting that the card is interactive |
Import the exact TypeScript type behind any property above, see
Properties
JS-only — no matching HTML attribute, set these from a script or a template binding.
| Name | Type | Default | Description |
|---|---|---|---|
| href | string | — | 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 |
| download | string | — | 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. |
Slots
| Name | Description |
|---|---|
| (default) | The front layer's content, always visible at rest. Never make this a link |
| back | The 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
| Name | Type | Description |
|---|---|---|
| eunnavigate | NavigationEvent | Fired 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
CSS custom properties
| Name | Description |
|---|---|
| --sliding-card-width | Sets the width of the card |
| --sliding-card-height | Sets the height of the card |
| --sliding-card-slide-offset | Sets how far the front layer slides to reveal the back |
| --sliding-card-duration | Sets the duration of the slide and hint animations |
| --sliding-card-border-radius | Sets the corner radius of both layers |
| --sliding-card-back-inset | Sets 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-padding | Sets the padding 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-fade-height | Sets the height of the bottom fade, and the boundary past which the slid front layer is never rendered |
| --sliding-card-focus-outline-color | Sets the focus outline color of the front layer's toggle control, touch devices only |
| --card-link-badge-offset | Sets the inset of the external link badge from the back layer's top and right edges |
| --card-link-badge-size | Sets the diameter of the external link badge |
| --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 |
CSS shadow parts
| Name | Description |
|---|---|
| back | The 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.
A short summary of what changed this week, sent every Monday.
<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.
Bordered. Hover to see the border turn primary.
A muted tinted background, no border. Hover to see it deepen.
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.
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
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 utilityrow-gap to match a title sitting
right above its description.
Sign up with your email, it takes less than a minute.
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.
Product Designer
<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.
Everything in Free, plus priority support and custom domains.
Unlimited projects Priority support Custom domains
<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.
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.
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.
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.
Opens on this same page.
Opens in a new tab.
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).
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 agridcellper 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. Addaria-hidden="true"yourself on genuinely decorative content. Leave it alone (or add your ownaria-label) when it conveys something unique. eun-cardenforces no heading level of its own. Slot in whatever heading (h2–h6) fits the surrounding page's own outline, the same wayeun-image-card'stitleslot 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 getsrole="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