react-ttrpg-cards
v0.3.1
Published
Plug-and-play React component for dealing 3D playing cards with physics
Downloads
667
Maintainers
Readme
🃏 react-ttrpg-cards
Plug-and-play React component for dealing 3D playing cards with physics.
Drop a full-deck card dealing experience into any React app. Cards spawn off-screen and fly onto a target element with realistic Rapier physics — complete with procedural textures, five built-in themes, multiple deal patterns, and synthesised sound effects. Zero image assets required.
✨ Features
- Physics-driven dealing — Cards launch with impulse forces, tumble through the air, and settle naturally using Rapier rigid-body physics.
- 5 deal patterns —
fan,stack,hand,row, andreveal— each with staggered timing and organic randomisation. - 5 built-in themes —
classic,dark,ornate,minimalist, androyalwith full PBR material support. - Procedural card textures — Standard pips, ranks, and card backs are canvas-generated at runtime. No sprite sheets needed.
- Flexible texture modes —
procedural,procedural-bg,svg, andsvg-bgfor mixing generated graphics with custom assets. - Synthesised sound — Web Audio card snap, slide, flip, and shuffle sounds. No audio files to bundle.
- Flip & clear — Flip dealt cards face-up ↔ face-down, then fade-clear the table.
- Target-aware — Cards land relative to any DOM element's bounding rect, with live resize tracking.
- Accessible — ARIA live regions announce deals; respects
prefers-reduced-motion. - TypeScript-first — Full type exports with discriminated unions for card identity.
- React 18 + 19 — Works with both major React versions.
"use client"— SSR/RSC safe out of the box.
📦 Installation
npm install react-ttrpg-cardsPeer dependencies
These must be installed in your project:
npm install react react-dom three @react-three/fiber @react-three/rapier| Package | Version |
|----------------------|---------------|
| react | ^18 \|\| ^19 |
| react-dom | ^18 \|\| ^19 |
| three | ≥ 0.171.0 |
| @react-three/fiber | ≥ 9.0.0 |
| @react-three/rapier| ≥ 1.5.0 |
🚀 Quick Start
import { useRef } from 'react';
import { useCardDeal } from 'react-ttrpg-cards';
import type { Card } from 'react-ttrpg-cards';
function App() {
const tableRef = useRef<HTMLDivElement>(null);
const { deal, isDealing, result, CardOverlayPortal } = useCardDeal({
targetRef: tableRef,
});
const cards: Card[] = [
{ suit: 'spades', rank: 'A' },
{ suit: 'hearts', rank: 'K' },
{ suit: 'diamonds', rank: 'Q' },
];
return (
<>
<div ref={tableRef} style={{ width: 600, height: 400 }}>
Game Table
</div>
<button onClick={() => deal(cards, { pattern: 'fan' })} disabled={isDealing}>
Deal
</button>
{CardOverlayPortal}
</>
);
}Important:
{CardOverlayPortal}must be rendered somewhere in your component tree. It creates a fixed full-screen canvas overlay that the 3D cards render into.
📖 API Reference
useCardDeal(options): UseCardDealReturn
The primary hook for dealing cards.
Options
interface UseCardDealOptions {
/** Ref to the HTML element cards will land on (required) */
targetRef: React.RefObject<HTMLElement>;
/** Theme configuration */
config?: CardThemeConfig;
/** Texture rendering mode */
textureConfig?: TextureConfig;
/** Camera angle offset */
cameraAngle?: CameraAngle;
/** Enable sound — `true` for defaults, or pass a config object */
sound?: boolean | CardSoundConfig;
/** Hard timeout (ms) before forcing result. Default: 6000 */
timeout?: number;
/** CSS z-index for the overlay canvas. Default: 9999 */
zIndex?: number;
/** Callback when all cards have settled */
onDealComplete?: (result: DealResult) => void;
}Return value
interface UseCardDealReturn {
/** Deal a set of cards */
deal: (cards: Card[], opts?: DealOptions) => void;
/** Flip specific cards by index (toggles face-up ↔ face-down) */
flip: (indices: number[]) => void;
/** Fade out and remove all cards */
clear: () => void;
/** True while cards are animating / settling */
isDealing: boolean;
/** Latest deal result (persists until clear() is called) */
result: DealResult | null;
/** Render this in your component tree to display the 3D overlay */
CardOverlayPortal: React.ReactNode;
}Deal options
interface DealOptions {
/** Layout pattern. Default: 'fan' */
pattern?: 'fan' | 'stack' | 'hand' | 'row' | 'reveal';
/** Deal face-up or face-down. Default: true */
faceUp?: boolean;
}<CardOverlay /> (low-level)
The underlying component if you need full control. Most users should prefer useCardDeal.
<CardOverlay
cards={cards}
pattern="fan"
faceUp={true}
targetRef={tableRef}
onDealComplete={(result) => console.log(result)}
config={{ theme: 'dark' }}
sound={{ volume: 0.5 }}
/>🃏 Card Types
Cards use a discriminated union:
// Standard playing card
interface StandardCard {
suit: 'hearts' | 'diamonds' | 'clubs' | 'spades';
rank: 'A' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | 'J' | 'Q' | 'K';
}
// Joker
interface JokerCard {
type: 'joker';
variant?: 'red' | 'black';
}
type Card = StandardCard | JokerCard;Type guards are exported for convenience:
import { isStandardCard, isJokerCard } from 'react-ttrpg-cards';
if (isStandardCard(card)) {
console.log(card.suit, card.rank);
}🎨 Themes
Five built-in themes control card face colours, pip colours, edge colour, card back pattern, and PBR material properties:
| Theme | Card Face | Back Pattern | Character |
|--------------|-----------|-------------|-----------------------|
| classic | White | Crosshatch | Traditional blue & gold |
| dark | Dark navy | Diamond | Sleek dark mode |
| ornate | Warm cream| Ornate | Vintage, gilded edges |
| minimalist | Pure white| Solid | Clean, modern |
| royal | Off-white | Royal | Regal blue & gold |
const { deal, CardOverlayPortal } = useCardDeal({
targetRef: tableRef,
config: {
theme: 'dark',
// Override individual colours:
redPipColor: '#ff6b6b',
edgeColor: '#333',
// Custom card back image (overrides theme pattern):
backImage: '/my-card-back.png',
// Corner radius as fraction of half-height:
cornerRadius: 0.08,
},
});🖼️ Texture Modes
Control how card face textures are generated:
| Mode | Description |
|-----------------|--------------------------------------------------|
| procedural | Pure canvas-generated pips & ranks (default, zero assets) |
| procedural-bg | Procedural overlay on a JPG/PNG background image |
| svg | User-supplied SVG foreground replaces procedural pips |
| svg-bg | SVG foreground composited over a background image |
const { deal, CardOverlayPortal } = useCardDeal({
targetRef: tableRef,
textureConfig: {
mode: 'procedural-bg',
backgroundImage: '/card-texture.jpg',
},
});🎯 Deal Patterns
| Pattern | Description |
|----------|--------------------------------------------------------|
| fan | Cards arc in a semicircular spread |
| stack | Cards dealt to a slightly offset pile |
| hand | Curved held-hand formation with slight overlap |
| row | Evenly spaced horizontal line |
| reveal | Single dramatic face-up reveal (or small set) |
deal(cards, { pattern: 'reveal', faceUp: true });🔊 Sound
All sounds are procedurally synthesised using the Web Audio API — no audio files needed.
| Sound | Trigger |
|----------|-------------------------------|
| shuffle | Automatic at deal start |
| deal | Each card hits the surface |
| slide | Card slides along the table |
| flip | Card flips face-up / face-down|
// Enable with defaults
const hook = useCardDeal({ targetRef, sound: true });
// Custom volume (0–1)
const hook = useCardDeal({ targetRef, sound: { volume: 0.5 } });
// Disable (default)
const hook = useCardDeal({ targetRef, sound: false });📸 Camera
Adjust the camera viewing angle with small offsets:
const hook = useCardDeal({
targetRef,
cameraAngle: {
x: 2, // horizontal offset (left/right tilt)
z: 3, // depth offset (forward/backward tilt)
},
});♻️ Flip & Clear
const { deal, flip, clear, result } = useCardDeal({ targetRef });
// Deal face-down
deal(cards, { faceUp: false });
// Later — flip the first and third cards face-up
flip([0, 2]);
// Clear the table with a fade-out
clear();🏗️ Architecture
src/
├── index.ts # Public API exports
├── types.ts # All TypeScript types + type guards
├── use-card-deal.tsx # Primary hook
├── components/
│ ├── card-overlay.tsx # Full-screen Canvas + orthographic camera
│ ├── physics-scene.tsx # Rapier Physics world + card spawning
│ └── card-body.tsx # Individual card rigid body + materials
├── animation/
│ ├── deal-patterns.ts # Spawn config generators for each pattern
│ └── flip-animation.ts # Card flip animation logic
├── geometry/
│ └── card-geometry.ts # Rounded-rect card mesh with UV mapping
├── textures/
│ ├── texture-generator.ts # Procedural card face renderer
│ └── back-texture.ts # Card back pattern renderer
├── themes/
│ ├── theme-definitions.ts # Built-in theme colour palettes
│ └── apply-theme.ts # Theme merging + override logic
└── sound/
└── card-sound.ts # Web Audio procedural sound engine🛠️ Development
# Install dependencies
npm install
# Build the library (uses bunchee)
npm run build
# Watch mode
npm run dev
# Run tests
npm test
# Type-check
npm run typecheckRunning the demo
cd demo
npm install
npm run devThe demo app provides a full interactive playground with theme switching, deal pattern selection, target zone presets, card picking, flip, and clear controls.
📋 Requirements
- Node.js ≥ 20
- React 18 or 19
- A browser with WebGL support
