CardDetail
Composable card detail experience with independently optional facts, collection state, and market history.

M Venusaur EX
Market Price
Your collection

Lightly Played
Jun 18, 2026$24.00$27.97
Details
- Type
- HP
- 230
- Stage
- Mega
- Card text
- When 1 of your Pokemon becomes a Mega Evolution Pokemon, your turn ends.
- Your opponent's Active Pokemon is now Paralyzed and Poisoned.
- Attack
- Crisis Vine - 120. Your opponent's Active Pokemon is now Paralyzed and Poisoned.
- Weakness
"use client"; import { useState } from "react";import { CardDetail, type CardDetailFact } from "@cardspark/ui/layouts"; const card = { id: "evo-100", name: "M Venusaur EX", set: "XY - Evolutions", setCode: "EVO", number: "100/108", rarity: "Ultra Rare", language: "EN", imageUrl: "https://images.pokemontcg.io/xy12/100_hires.png", hp: "230", type: "Grass", stage: "Mega"}; const facts: CardDetailFact[] = [ { kind: "type", value: "Grass" }, { kind: "text", label: "HP", value: "230" }, { kind: "text", label: "Attack", value: "Crisis Vine - 120. Your opponent's Active Pokemon is now Paralyzed and Poisoned." }, { kind: "weakness", value: "Fire x2" }]; const collection = { actionLabel: "Add to collection", items: [ { id: "evo-100-lp", name: "Lightly Played", set: "XY - Evolutions", setCode: "EVO", number: "100/108", rarity: "Ultra Rare", condition: "Lightly Played", value: "$27.97", imageUrl: card.imageUrl } ]}; const market = { label: "Market price", price: "$27.97", deltaToday: "+2.16%"}; const history = { values: [26.55, 27.1, 26.82, 27.32, 27.05, 27.74, 27.97], baselineValue: 27.38, range: "1D", ranges: ["1D", "1W", "1M", "1Y", "ALL"]}; export function CardDetailDemo() { const variants = ["Normal", "Holofoil", "Reverse Holofoil"]; const conditions = ["Near Mint", "Lightly Played", "Moderately Played"]; const dataSources = ["All", "Raw", "TCGplayer", "eBay"]; const graders = ["Ungraded", "PSA", "CGC"]; const grades = ["10", "9", "8"]; const [activeVariant, setActiveVariant] = useState("Holofoil"); const [activeCondition, setActiveCondition] = useState("Lightly Played"); const [activeDataSource, setActiveDataSource] = useState("All"); const [activeGrader, setActiveGrader] = useState("Ungraded"); const [activeGrade, setActiveGrade] = useState("10"); const [activeRange, setActiveRange] = useState("1D"); const [isOwned, setIsOwned] = useState(false); return ( <CardDetail card={card} facts={facts} variants={variants} activeVariant={activeVariant} conditions={conditions} activeCondition={activeCondition} dataSources={dataSources} activeDataSource={activeDataSource} graders={graders} activeGrader={activeGrader} grades={grades} activeGrade={activeGrade} collection={{ ...collection, actionLabel: isOwned ? "In collection" : collection.actionLabel, items: isOwned ? collection.items : [] }} market={market} history={{ ...history, range: activeRange }} onCollectionAction={() => setIsOwned(true)} onConditionChange={setActiveCondition} onDataSourceChange={setActiveDataSource} onGraderChange={setActiveGrader} onGradeChange={setActiveGrade} onVariantChange={setActiveVariant} onRangeChange={setActiveRange} /> );}Installation
Install the package.
npm install @cardspark/uiImport the shared stylesheet layer near your app root.
app/layout.tsx
import "@cardspark/ui/tokens.css";import "@cardspark/ui/themes/basement.css";import "@cardspark/ui/styles.css";Import and render CardDetail from its package entry point.
Usage
CardDetailDemo.tsx
"use client"; import { useState } from "react";import { CardDetail, type CardDetailFact } from "@cardspark/ui/layouts"; const card = { id: "evo-100", name: "M Venusaur EX", set: "XY - Evolutions", setCode: "EVO", number: "100/108", rarity: "Ultra Rare", language: "EN", imageUrl: "https://images.pokemontcg.io/xy12/100_hires.png", hp: "230", type: "Grass", stage: "Mega"}; const facts: CardDetailFact[] = [ { kind: "type", value: "Grass" }, { kind: "text", label: "HP", value: "230" }, { kind: "text", label: "Attack", value: "Crisis Vine - 120. Your opponent's Active Pokemon is now Paralyzed and Poisoned." }, { kind: "weakness", value: "Fire x2" }]; const collection = { actionLabel: "Add to collection", items: [ { id: "evo-100-lp", name: "Lightly Played", set: "XY - Evolutions", setCode: "EVO", number: "100/108", rarity: "Ultra Rare", condition: "Lightly Played", value: "$27.97", imageUrl: card.imageUrl } ]}; const market = { label: "Market price", price: "$27.97", deltaToday: "+2.16%"}; const history = { values: [26.55, 27.1, 26.82, 27.32, 27.05, 27.74, 27.97], baselineValue: 27.38, range: "1D", ranges: ["1D", "1W", "1M", "1Y", "ALL"]}; export function CardDetailDemo() { const variants = ["Normal", "Holofoil", "Reverse Holofoil"]; const conditions = ["Near Mint", "Lightly Played", "Moderately Played"]; const dataSources = ["All", "Raw", "TCGplayer", "eBay"]; const graders = ["Ungraded", "PSA", "CGC"]; const grades = ["10", "9", "8"]; const [activeVariant, setActiveVariant] = useState("Holofoil"); const [activeCondition, setActiveCondition] = useState("Lightly Played"); const [activeDataSource, setActiveDataSource] = useState("All"); const [activeGrader, setActiveGrader] = useState("Ungraded"); const [activeGrade, setActiveGrade] = useState("10"); const [activeRange, setActiveRange] = useState("1D"); const [isOwned, setIsOwned] = useState(false); return ( <CardDetail card={card} facts={facts} variants={variants} activeVariant={activeVariant} conditions={conditions} activeCondition={activeCondition} dataSources={dataSources} activeDataSource={activeDataSource} graders={graders} activeGrader={activeGrader} grades={grades} activeGrade={activeGrade} collection={{ ...collection, actionLabel: isOwned ? "In collection" : collection.actionLabel, items: isOwned ? collection.items : [] }} market={market} history={{ ...history, range: activeRange }} onCollectionAction={() => setIsOwned(true)} onConditionChange={setActiveCondition} onDataSourceChange={setActiveDataSource} onGraderChange={setActiveGrader} onGradeChange={setActiveGrade} onVariantChange={setActiveVariant} onRangeChange={setActiveRange} /> );}Composition
Use CardDetail for a focused card surface, supplying only the facts, collection, and market-history sections the page has.
CardDetailPage└── CardDetail ├── card identity and artwork ├── variant, condition, source, grader, and grade controls ├── optional collection state and owned rows ├── optional structured facts and rules text └── optional market summary and price historyPass `market` and `history` together to render price history. Type, weakness, and rules facts use explicit `kind` values; other facts use `kind="text"`.
Features
- Composes canonical card data with independently optional
collection,marketplushistory, and structuredfactssections. - Keeps grader and grade state separate while hiding condition or grade controls when they do not apply.
- Forwards native article props and refs and exposes focused callbacks for each controlled selection.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
card | CardDetailCard | - | Canonical card requiring stable `id`, `name`, `set`, `number`, and `rarity` fields. |
facts | CardDetailFact[] | [] | Structured text, type, weakness, and rules facts rendered when supplied. |
variants | string[] | - | Variant options available to the detail controls. |
activeVariant | string | - | Currently selected card variant. |
conditions | string[] | - | Condition options available to the detail controls. |
activeCondition | string | - | Currently selected condition value. |
grades | string[] | [] | Grade options available when a graded grader is active. |
activeGrade | string | - | Currently selected numeric or named grade. |
dataSources | string[] | - | Data source options available to the detail controls. |
activeDataSource | string | - | Currently selected data source value. |
graders | string[] | - | Grader options available to the detail controls. |
activeGrader | string | - | Currently selected grader value. |
collection | CardDetailCollection | - | Optional collection action and stable-id owned-copy rows. |
market | CardDetailMarketSummary | - | Formatted market summary. Rendered with price history only when `history` is also supplied. |
history | CardDetailHistory | - | Optional chart values, ranges, and dimension-specific data sets; rendered when `market` is also supplied. |
onCollectionAction | (event: MouseEvent<HTMLElement>) => void | - | Callback fired with the native event when the primary collection action is activated. |
onConditionChange | (condition: string) => void | - | Callback fired when the active condition changes. |
onGradeChange | (grade: string) => void | - | Callback fired when the active grade changes. |
onDataSourceChange | (dataSource: string) => void | - | Callback fired when the active market data source changes. |
onGraderChange | (grader: string) => void | - | Callback fired when the active grader changes. |
onVariantChange | (variant: string) => void | - | Callback fired when the active variant changes. |
onRangeChange | (range: string) => void | - | Callback fired when a chart range changes. |