1
ComponentsForm Controls

Selectable Card

A card-shaped button with controlled selected state, keyboard focus and pressed semantics.

Purpose

Use Selectable Card for a single card-shaped choice when the whole surface should act as one button and expose its selected state.

Card choices

"use client";

import { useState } from "react";
import { SelectableCard } from "@/components/ui/selectable-card";

export function Example() {
  const [selected, setSelected] = useState("team");

  return (
    <>
      <SelectableCard onClick={() => setSelected("team")} selected={selected === "team"}>Team workspace</SelectableCard>
      <SelectableCard onClick={() => setSelected("personal")} selected={selected === "personal"}>Personal workspace</SelectableCard>
    </>
  );
}

Installation

With the Siteplane registry alias configured, add the component and import the installed source from your app.

CLI

pnpm dlx shadcn@4.16.1 add @siteplane/selectable-card

Import

import { SelectableCard } from "@/components/ui/selectable-card";

Need the machine-readable source metadata? View registry JSON.

Usage

Selectable Card turns the whole surface into a selectable control while keeping button semantics.

"use client";

import { useState } from "react";
import { SelectableCard } from "@/components/ui/selectable-card";

export function Example() {
  const [selected, setSelected] = useState("team");

  return (
    <>
      <SelectableCard onClick={() => setSelected("team")} selected={selected === "team"}>Team workspace</SelectableCard>
      <SelectableCard onClick={() => setSelected("personal")} selected={selected === "personal"}>Personal workspace</SelectableCard>
    </>
  );
}

Examples

Selection always stays in the calling flow: control selected via state and onClick.

States

{/* Default */}
<SelectableCard>Starter</SelectableCard>

{/* Selected */}
<SelectableCard selected>Team</SelectableCard>

{/* Disabled */}
<SelectableCard disabled>Scale</SelectableCard>

Plan choice

const [selectedPlan, setSelectedPlan] = useState("team");

<div className="grid gap-2">
  {planOptions.map((option) => (
    <SelectableCard
      key={option.value}
      onClick={() => setSelectedPlan(option.value)}
      selected={selectedPlan === option.value}
    >
      {option.label}
    </SelectableCard>
  ))}
</div>

Compact choice

Density stays a className decision on the same primitive.

<SelectableCard className="px-3 py-2" onClick={() => setSelectedValue("auto")} selected={selectedValue === "auto"}>
  Auto
</SelectableCard>
<SelectableCard className="px-3 py-2" onClick={() => setSelectedValue("manual")} selected={selectedValue === "manual"}>
  Manual
</SelectableCard>

API Reference

The reference lists the source-owned exports and props that are easy to miss in visual examples. Inherited Base UI props remain available unless the wrapper narrows them.

  • SelectableCard renders a button by default, accepts Base UI's render prop and exposes a controlled selected boolean.
  • The component mirrors selected to data-selected and aria-pressed; update the value from the consumer's click handler.
  • Native button props such as disabled, type, accessible naming and keyboard activation remain available.

Motion

Component motion uses the shared Siteplane motion contract. See the global motion guide for provider setup, tokens and reduced-motion behavior.

  • Selection, hover and focus use color, border and ring transitions without moving the card.
  • Reduced motion requires no additional override because the component has no scale or translation animation.

Accessibility

  • Keep the card as one semantic button with a clear accessible name.
  • Do not nest buttons, links or other interactive controls inside a clickable Selectable Card.
  • aria-pressed describes the selected state; do not replace it with color-only styling.

Implementation Guidance

  • Use Radio Group for a mutually exclusive list with native radio semantics and Checkbox for independent multiple selection.
  • Keep state controlled by the caller and preserve the button contract.
  • Do not hand-build a clickable card with a non-focusable container.

On This Page