Card
Card groups the content about one thing, such as a course, space or lesson. Make the whole card a link or button with a stretched title, and keep extra actions beside it.
Beta until design confirms the code-only focus, selected and disabled states, the md padding and the description style, and the focus halo contrast (C5) is resolved.
Preview
Change the controls to try each combination. Click the card or press Enter on its title; the ⋯ menu works separately.
<Card>
<CardBody>
<CardHeader>
<CardTitle href="/spaces/ui-ux">UI/UX Design</CardTitle>
<CardActions>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<CardActionButton label="Options for UI/UX Design"><RiMoreFill /></CardActionButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">…</DropdownMenuContent>
</DropdownMenu>
</CardActions>
</CardHeader>
<CardMeta>
<CardMetaItem icon={<RiFileTextLine />}>12 lessons</CardMetaItem>
</CardMeta>
</CardBody>
<CardFooter>Updated 4 seconds ago</CardFooter>
</Card>Installation
Availability
@nuton/ui and @nuton/tokens are private workspace packages and aren't published yet (D4, D12). Inside the Nuton monorepo, add them as workspace dependencies.
pnpm add @nuton/ui @nuton/tokens @remixicon/reactnpm install @nuton/ui @nuton/tokens @remixicon/reactyarn add @nuton/ui @nuton/tokens @remixicon/reactimport '@nuton/tokens/tokens.css'
import '@nuton/ui/styles.css'Usage
import { Card, CardBody, CardFooter, CardHeader, CardMeta, CardMetaItem, CardTitle } from '@nuton/ui'
import { RiFileTextLine } from '@remixicon/react'
<Card>
<CardBody>
<CardHeader>
<CardTitle href="/spaces/ui-ux">UI/UX Design</CardTitle>
</CardHeader>
<CardMeta>
<CardMetaItem icon={<RiFileTextLine />}>12 lessons</CardMetaItem>
</CardMeta>
</CardBody>
<CardFooter>Updated 4 seconds ago</CardFooter>
</Card>- Give
CardTitleanhrefto make the whole card a link, oronClickwhen nothing has a URL. The title text is the accessible name, so keep it short and unique. - Put the ⋯ menu and other buttons in
CardActions. They sit beside the main link and above its click area, so nothing is nested inside a link or button. - Never wrap a Card in
<a>or<button>, and never addrole="button"to it. That is what caused the nested-control and keyboard bugs on the live site. - Use
as="li"inside a<ul>for grids, and setCardTitle levelso titles fit the page's heading outline. - For router links (Next.js
Link), passlinkProps={{ as: Link }}to CardTitle.
Variants and sizes
Cards have one radius (12, from both Figma cards), three paddings and two title sizes. Everything else is composition.
Padding
<Card padding="sm">…</Card> {/* 12, Figma Suggestion Card */}
<Card padding="md">…</Card> {/* 16, code-only */}
<Card>…</Card> {/* lg 24, Figma Space Cards (default) */}Suggestion layout
Engineering basics
Introduction to Turbomachinery
<Card padding="sm">
<CardHeader>
<div>
<CardEyebrow>Engineering basics</CardEyebrow>
<CardTitle size="sm" href="/learn/turbo">Introduction to Turbomachinery</CardTitle>
</div>
<CardIndicator><RiArrowRightUpLine /></CardIndicator>
</CardHeader>
</Card>Static surface
Weekly goal
Study 3 hours across any course.
<Card padding="md">
<CardTitle level={3}>Weekly goal</CardTitle>
<CardDescription>Study 3 hours across any course.</CardDescription>
</Card>Without an href or onClick, a Card is a plain surface: no hover, no focus stop.
| Part | Figma value | Token / style |
|---|---|---|
| Card | Radius 12; padding 24 (Space) or 12 (Suggestion); 1px inside stroke; no shadow | --nu-radius-12, --nu-space-6 / -3, Stroke/Soft-200, Bg/White |
| Gap between parts | 24 (Space card); follows padding | --nu-card-gap |
| Body column | Gap 8 | --nu-space-2 |
| Header row | Gap 10, items centred | --nu-card-header-gap |
| Title | 16/28 medium (md) · 14/20 medium (sm) | Label/Medium · Label/Small, Text/Strong-950 |
| Eyebrow | 12/20 medium | Label/X Small, Text/Sub-600 (Figma Soft-400, D7) |
| Meta item | 16px icon, gap 4, 14/24 | Paragraph/Small, Text/Sub-600, Icon/Sub-600 |
| Footer | 12/16 | Paragraph/X Small, Text/Sub-600 |
| Action button | 28 × 28, padding 4, radius 8, 20px icon | --nu-card-action-size, --nu-radius-8 |
| Indicator | 24px arrow-right-up-line | Icon/Sub-600 |
States and interactions
aria-current="true", buttons aria-pressed.aria-disabled.State tokens
Set on .nu-card.
| Token | Value | Used for |
|---|---|---|
--nu-card-bg | Bg/White | Default, selected, disabled |
--nu-card-bg-hover | Bg/Weak-50 | Hover |
--nu-card-stroke | Stroke/Soft-200 | Border |
--nu-card-stroke-selected | State/Primary/Base | Selected and focus border |
--nu-card-focus-shadow | buttons/primary-focus | Focus halo (C5) |
--nu-card-radius · --nu-card-pad · --nu-card-gap | 12 · 24 · 24 | Shape and spacing |
Gaps in Figma
- Figma has no focused, selected or disabled card, no description style and no md padding. These are code-only choices, listed above.
- Figma colours the Suggestion Card eyebrow Text/Soft-400 (2.49:1). Code uses Text/Sub-600 so it meets 4.5:1 (D7).
- Live cards use radius 12, 16, 20 and 24. Code uses 12 for every app card, from both Figma cards.
Composition examples
Spaces grid
Each card is a list item. Tab moves to a card's title link, then to its ⋯ menu, then to the next card.
Course card with description, meta and badge
Lesson suggestions
API reference
Card props
| Prop | Type | Default | Description |
|---|---|---|---|
padding | 'sm' | 'md' | 'lg' | 'lg' | Inner padding 12 / 16 / 24. The gap between parts follows it. |
orientation | 'vertical' | 'horizontal' | 'vertical' | horizontal puts CardMedia beside a CardBody column. |
selected | boolean | false | Primary stroke; the main link gets aria-current="true" (or aria-pressed on a button). |
disabled | boolean | false | Disables the main action and the hover. Actions in CardActions are yours to disable. |
as | 'div' | 'article' | 'section' | 'li' | 'div' | Element to render. Use li inside a ul. |
…HTML props | HTMLAttributes | — | Passed to the card element. The ref points at it. |
CardTitle and CardLink props
| Prop | Type | Default | Description |
|---|---|---|---|
level | 2 | 3 | 4 | 5 | 6 | 3 | Heading level. |
size | 'sm' | 'md' | 'md' | md = Label/Medium 16/28; sm = Label/Small 14/20. |
href | string | — | Makes the title the stretched main link. |
onClick | MouseEventHandler | — | Makes the title a stretched button when there is no href; with href, runs on click. |
linkProps | CardLinkProps | — | Extra props for the inner CardLink, e.g. { as: Link, target }. |
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | — | Renders a link. Without it, a <button type="button">. |
onClick | MouseEventHandler | — | Click handler. |
as | ElementType | — | Link component, e.g. Next.js Link. Receives href, className and ref. |
target · rel · download | string | — | Link attributes. |
disabled | boolean | — | Defaults to the Card’s disabled. |
Other parts
| Prop | Type | Default | Description |
|---|---|---|---|
CardMedia | { decorative?: boolean } | decorative: true | Thumbnail or leading visual. Hidden from assistive tech unless decorative={false}. |
CardBody | HTMLAttributes<div> | — | Text column (gap 8). |
CardHeader | HTMLAttributes<div> | — | Title row with actions or an indicator at the end. |
CardEyebrow | HTMLAttributes<p> | — | Small label above the title. |
CardDescription | HTMLAttributes<p> | — | Secondary text. |
CardMeta · CardMetaItem | { icon?: ReactNode } | — | Row of facts, each with an optional 16px decorative icon. |
CardBadges | HTMLAttributes<div> | — | Badges or Tags; sits above the click area. |
CardFooter | HTMLAttributes<div> | — | Bottom line, e.g. last updated. |
CardActions | HTMLAttributes<div> | — | Secondary controls, above the click area. |
CardActionButtonrequired | { label: string; children: ReactNode } | — | 28px icon-only button; label is its accessible name. Works with DropdownMenuTrigger asChild. |
CardIndicator | HTMLAttributes<span> | — | Decorative 24px trailing icon. |
Accessibility
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Moves to the card’s main link or button, then to each control in CardActions |
| Enter | Follows the link (or activates the button / opens the menu) |
| Space | Activates the button form and action buttons |
Screen readers
One link or button per card, named by the title only, inside a heading. Meta, description and footer are read as text. Icons and media are hidden. The ⋯ button has its own name and is a sibling of the link, never inside it.
Focus
Focus sits on the title link; the whole card shows the primary stroke and halo while it does. The action button has its own focus halo.
Contrast and colour
Title Strong-950 (17.9:1); meta, eyebrow, description and footer Sub-600 (6.7:1) instead of the live site's Soft-400 (2.49:1). Selected adds a stroke, and the link also exposes the state in code.
Touch and pointer
The whole card is the target. The action button is 28 × 28, above the 24 × 24 minimum (WCAG 2.5.8).
You must: give every card a unique title, name each action button with the item ("Options for UI/UX Design"), and never put a link or button inside CardTitle.
Figma mapping
| Figma | Values | Code |
|---|---|---|
| Space Cards · Property 1 | Default · Variant2 | Default and hover |
| Space Cards title, ⋯ button | Label/Medium · 28px button | CardTitle · CardActionButton in CardActions |
| Space Cards “0 lessons”, “Updated …” | Paragraph/Small · Paragraph/X Small | CardMetaItem · CardFooter |
| Suggestion Card · Property 1 | Default · Hover | padding="sm", default and hover |
| Suggestion Card · Title (boolean) | Eyebrow on/off | CardEyebrow |
| Suggestion Card arrow | arrow-right-up-line 24 | CardIndicator |
Guidelines and related components
Use Card for
- Collections of courses, spaces, research papers or suggestions
- Grids where each item opens its own page
- Grouping a small block of related content
Don't use Card for
- Picking options in a form (use Checkbox Card or Radio Card)
- Dense lists of many items (use a list with dividers)
- A single call to action (use Button)
Do and don't
Cell Biology
Imagine this whole card as role="button" with the ⋯ button inside it.