Mode Card

A selectable tile that represents one mode in a small picker — study mode, difficulty preset, theme variant, layout density. Tapping the card toggles its selected state; the active card paints the accent surface, inactive cards paint a neutral surface with a hairline border.

Customize
Size
lg
State
Content

Installation

npx shadcn@latest add https://craftbits.dev/r/mode-card.json

Usage

import { ModeCard } from "@craft-bits/core";
import { Zap } from "lucide-react";
 
<ModeCard
  icon={<Zap size={20} />}
  title="Practice"
  description="Drill the trickier sub-skills."
  selected={picked === "practice"}
  onClick={() => setPicked("practice")}
/>

A radio-group picker:

<div role="radiogroup" aria-label="Study mode" className="flex flex-col gap-3">
  {modes.map((m) => (
    <ModeCard
      key={m.id}
      icon={<m.Icon size={20} />}
      title={m.title}
      description={m.description}
      selected={picked === m.id}
      onClick={() => setPicked(m.id)}
    />
  ))}
</div>

A compact grid of size="sm" chips:

<div className="grid grid-cols-3 gap-2">
  {densities.map((d) => (
    <ModeCard
      key={d.id}
      size="sm"
      icon={<d.Icon size={14} />}
      title={d.title}
      description={d.description}
      selected={picked === d.id}
      onClick={() => setPicked(d.id)}
    />
  ))}
</div>

Anatomy

  • Icon — top-left (lg) or top-center (sm). Inherits currentColor so it tracks the selected vs idle text tone.
  • Title — serif heading. Short labels read best (Study, Practice, Review).
  • Description — small supporting line. Tone flips between opacity-80 on the accent fill and cb-fg-muted / cb-fg-subtle on the idle surface.
  • Surfacebg-cb-accent when selected; bg-cb-bg-elevated with a hairline cb-border when idle.

Sizes

  • lg (default) — row-style card: icon on the left, title above description on the right. The "primary" picker treatment.
  • sm — column-style chip: icon stacked above title and description. The "secondary" / grid-of-options treatment.

Props

PropTypeDefaultDescription
titleReactNoderequiredHeading.
descriptionReactNodeOne-line supporting text under the title.
iconReactNodeInline icon — lucide-react glyph or any SVG.
selectedbooleanfalseWhether this card is the currently picked option.
size'lg' | 'sm''lg'Row-style or column-style.
disabledbooleanLocks the card. Drops to 40% opacity and suppresses the tap scale.
onClick(e) => voidClick handler. Caller owns the picker state.
classNamestringMerged onto the root via cn().

Accessibility

  • Renders as <button type="button" role="radio" aria-checked={selected}>. Wrap a stack in a role="radiogroup" element with an aria-label.
  • The icon is aria-hidden="true" — pair it with a meaningful title so the accessible name always conveys the mode.
  • disabled sets both the native disabled attribute and aria-disabled="true".
  • Focus visibly highlights via focus-visible:ring-cb-accent.

Credits

  • Extracted from: algoflashcards (src/platform/ui/ModeCard.tsx). The original used a variant: "primary" | "secondary" switch plus a disabled boolean and a free-floating index prop. The library version renames variant to size, drops index, exposes a first-class selected boolean instead of inferring active state from !disabled, replaces bevel-tile / bg-card with --cb-* semantic tokens, swaps SPRING.snappy for SPRINGS.snap, and migrates the inline whileTap to the canonical TAP_SCALE constant.