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
  • Accessibility
  • Figma Space Cards 2884:78459 · Suggestion Card 39:3122
  • packages/ui/src/components/Card

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.

12 lessons

tsx
<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.

bash
pnpm add @nuton/ui @nuton/tokens @remixicon/react
tsx
import '@nuton/tokens/tokens.css'
import '@nuton/ui/styles.css'

Usage

tsx
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 CardTitle an href to make the whole card a link, or onClick when 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 add role="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 set CardTitle level so titles fit the page's heading outline.
  • For router links (Next.js Link), pass linkProps={{ 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

0 lessons
0 lessons
0 lessons

Suggestion layout

Static surface

Weekly goal

Study 3 hours across any course.

Without an href or onClick, a Card is a plain surface: no hover, no focus stop.

PartFigma valueToken / style
CardRadius 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 parts24 (Space card); follows padding--nu-card-gap
Body columnGap 8--nu-space-2
Header rowGap 10, items centred--nu-card-header-gap
Title16/28 medium (md) · 14/20 medium (sm)Label/Medium · Label/Small, Text/Strong-950
Eyebrow12/20 mediumLabel/X Small, Text/Sub-600 (Figma Soft-400, D7)
Meta item16px icon, gap 4, 14/24Paragraph/Small, Text/Sub-600, Icon/Sub-600
Footer12/16Paragraph/X Small, Text/Sub-600
Action button28 × 28, padding 4, radius 8, 20px icon--nu-card-action-size, --nu-radius-8
Indicator24px arrow-right-up-lineIcon/Sub-600

States and interactions

0 lessons
DefaultBg/White with a 1px Stroke/Soft-200 border.
0 lessons
HoverBg/Weak-50 (Figma Variant2 / Hover). Only cards with a main action, only on hover-capable pointers.
Focus (code-only)When the main link has keyboard focus, the whole card gets the primary stroke and focus halo, as Checkbox Card (C5).
0 lessons
Selected (code-only)1px State/Primary/Base stroke. Links get aria-current="true", buttons aria-pressed.
0 lessons
Disabled (code-only)Title Text/Sub-600, no hover. The link loses its href and is aria-disabled.

State tokens

Set on .nu-card.

TokenValueUsed for
--nu-card-bgBg/WhiteDefault, selected, disabled
--nu-card-bg-hoverBg/Weak-50Hover
--nu-card-strokeStroke/Soft-200Border
--nu-card-stroke-selectedState/Primary/BaseSelected and focus border
--nu-card-focus-shadowbuttons/primary-focusFocus halo (C5)
--nu-card-radius · --nu-card-pad · --nu-card-gap12 · 24 · 24Shape 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

Reaction mechanisms, from SN1 to Diels–Alder.

8 chapters96 min
Research

Lesson suggestions

API reference

Card props

Card props
PropTypeDefaultDescription
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.
selectedbooleanfalsePrimary stroke; the main link gets aria-current="true" (or aria-pressed on a button).
disabledbooleanfalseDisables 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 propsHTMLAttributes—Passed to the card element. The ref points at it.

CardTitle and CardLink props

CardTitle props
PropTypeDefaultDescription
level2 | 3 | 4 | 5 | 63Heading level.
size'sm' | 'md''md'md = Label/Medium 16/28; sm = Label/Small 14/20.
hrefstring—Makes the title the stretched main link.
onClickMouseEventHandler—Makes the title a stretched button when there is no href; with href, runs on click.
linkPropsCardLinkProps—Extra props for the inner CardLink, e.g. { as: Link, target }.
CardLink props
PropTypeDefaultDescription
hrefstring—Renders a link. Without it, a <button type="button">.
onClickMouseEventHandler—Click handler.
asElementType—Link component, e.g. Next.js Link. Receives href, className and ref.
target · rel · downloadstring—Link attributes.
disabledboolean—Defaults to the Card’s disabled.

Other parts

Card parts
PropTypeDefaultDescription
CardMedia{ decorative?: boolean }decorative: trueThumbnail or leading visual. Hidden from assistive tech unless decorative={false}.
CardBodyHTMLAttributes<div>—Text column (gap 8).
CardHeaderHTMLAttributes<div>—Title row with actions or an indicator at the end.
CardEyebrowHTMLAttributes<p>—Small label above the title.
CardDescriptionHTMLAttributes<p>—Secondary text.
CardMeta · CardMetaItem{ icon?: ReactNode }—Row of facts, each with an optional 16px decorative icon.
CardBadgesHTMLAttributes<div>—Badges or Tags; sits above the click area.
CardFooterHTMLAttributes<div>—Bottom line, e.g. last updated.
CardActionsHTMLAttributes<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.
CardIndicatorHTMLAttributes<span>—Decorative 24px trailing icon.

Accessibility

Keyboard

KeyBehaviour
TabMoves to the card’s main link or button, then to each control in CardActions
EnterFollows the link (or activates the button / opens the menu)
SpaceActivates 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

FigmaValuesCode
Space Cards · Property 1Default · Variant2Default and hover
Space Cards title, ⋯ buttonLabel/Medium · 28px buttonCardTitle · CardActionButton in CardActions
Space Cards “0 lessons”, “Updated …”Paragraph/Small · Paragraph/X SmallCardMetaItem · CardFooter
Suggestion Card · Property 1Default · Hoverpadding="sm", default and hover
Suggestion Card · Title (boolean)Eyebrow on/offCardEyebrow
Suggestion Card arrowarrow-right-up-line 24CardIndicator

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

0 lessons
DoPut the ⋯ menu in CardActions, beside the title Two separate controls: one opens the space, one opens the menu.

Cell Biology

Imagine this whole card as role="button" with the ⋯ button inside it.

Don'tNest a button inside a clickable card A button inside role=button or a link is invalid; screen readers and keyboards can't reach it reliably.

Cell Biology

DoName actions with the item “Options for Cell Biology” makes sense out of context, in a list of buttons.

Open new space space

Don'tRepeat the item type in the title or link name The live site announced “Open new space space”. The title is the name; let the heading and list give context.