@salloomd/teeth-selector
v0.0.5
Published
A headless React component for dental tooth selection with bridge support. Follows the [shadcn/ui](https://ui.shadcn.com/) code style and patterns.
Maintainers
Readme
@salloomd/teeth-selector
A headless React component for dental tooth selection with bridge support. Follows the shadcn/ui code style and patterns.
Installation
bun add @salloomd/teeth-selector
# or
npm install @salloomd/teeth-selectorFeatures
- 🎨 Headless - Bring your own styles
- 🦷 Complete dental chart - All 32 teeth with FDI numbering
- 🌉 Bridge support - Select dental bridges between adjacent teeth
- 📱 Touch support - Drag selection works on mobile
- 🎯 Fully typed - Written in TypeScript
- ⚡ Render props - Full control over rendering
Usage
Basic Example (with Tailwind CSS)
import { TeethSelector, TeethChart } from "@salloomd/teeth-selector";
function MyDentalForm() {
return (
<TeethSelector
onChange={(selectedTeeth, bridges) => {
console.log("Selected:", selectedTeeth);
console.log("Bridges:", bridges);
}}
>
{({ selectedTeeth, clearAll, selectUpper, selectLower }) => (
<div className="space-y-4">
<div className="flex gap-2">
<button onClick={selectUpper}>Select Upper</button>
<button onClick={selectLower}>Select Lower</button>
{selectedTeeth.length > 0 && (
<button onClick={clearAll}>Clear All</button>
)}
</div>
<TeethChart
width={300}
height={500}
renderTooth={({ tooth, isSelected, isInDragRange }, defaultEl) => (
<path
key={tooth.id}
id={`teeth-${tooth.id}`}
d={tooth.d}
className={`
cursor-pointer transition-colors
${isSelected ? "fill-amber-400 stroke-gray-600" : "fill-white stroke-gray-400"}
${isInDragRange ? "fill-blue-300" : ""}
hover:fill-amber-200
`}
onClick={defaultEl.props.onClick}
onMouseDown={defaultEl.props.onMouseDown}
onMouseEnter={defaultEl.props.onMouseEnter}
/>
)}
renderBridge={({ fromId, toId, exists, position }, defaultEl) => (
<g key={`${fromId}-${toId}`}>
{exists && (
<line
x1={position.x1}
y1={position.y1}
x2={position.x2}
y2={position.y2}
stroke="#ff6b6b"
strokeWidth={4}
strokeDasharray="5,3"
/>
)}
<circle
cx={position.midX}
cy={position.midY}
r={8}
className={`
cursor-pointer transition-all
${
exists
? "fill-blue-500 stroke-white stroke-2"
: "fill-gray-200 stroke-gray-400 opacity-0 hover:opacity-100"
}
`}
onClick={defaultEl.props.children[1].props.onClick}
/>
</g>
)}
/>
</div>
)}
</TeethSelector>
);
}TeethPreview (Read-only)
import { TeethPreview } from "@salloomd/teeth-selector";
function Preview() {
// String format: individual teeth or bridged ranges
// "11, 21, [31, 33]" means teeth 11, 21, and bridged 31-32-33
return (
<TeethPreview
teeth="11, 21, [31, 33]"
width={150}
height={250}
renderTooth={({ tooth, isSelected }, defaultEl) => (
<path
key={tooth.id}
id={`teeth-preview-${tooth.id}`}
d={tooth.d}
className={
isSelected
? "fill-amber-400 stroke-gray-600"
: "fill-white stroke-gray-400"
}
/>
)}
/>
);
}Controlled Mode
import { useState } from "react";
import {
TeethSelector,
TeethChart,
type Tooth,
} from "@salloomd/teeth-selector";
function ControlledExample() {
const [selectedTeeth, setSelectedTeeth] = useState<number[]>([11, 21]);
const [bridges, setBridges] = useState<number[][]>([[11, 21]]);
return (
<TeethSelector
selectedTeeth={selectedTeeth}
bridges={bridges}
onSelectionChange={(teeth: Tooth[]) => {
setSelectedTeeth(teeth.map((t) => t.id));
}}
onBridgeChange={setBridges}
>
{(props) => (
<TeethChart
renderTooth={({ tooth, isSelected }, defaultEl) => (
<path
key={tooth.id}
id={`teeth-${tooth.id}`}
d={tooth.d}
style={{
fill: isSelected ? "#fbbf24" : "#fff",
stroke: isSelected ? "#4b5563" : "#9ca3af",
cursor: "pointer",
}}
onClick={defaultEl.props.onClick}
onMouseDown={defaultEl.props.onMouseDown}
onMouseEnter={defaultEl.props.onMouseEnter}
/>
)}
/>
)}
</TeethSelector>
);
}API Reference
TeethSelector
The root component that manages selection state.
| Prop | Type | Description |
| ---------------------- | ------------------------------------------------ | ----------------------------- |
| defaultSelectedTeeth | number[] | Initial selected teeth IDs |
| defaultBridges | number[][] | Initial bridges as pairs |
| selectedTeeth | number[] | Controlled selected teeth |
| bridges | number[][] | Controlled bridges |
| onSelectionChange | (teeth: Tooth[]) => void | Called when selection changes |
| onBridgeChange | (bridges: number[][]) => void | Called when bridges change |
| onChange | (teeth: Tooth[], bridges: number[][]) => void | Unified change callback |
| children | (props: TeethSelectorRenderProps) => ReactNode | Render function |
Render Props
interface TeethSelectorRenderProps {
selectedTeeth: number[];
bridges: number[][];
toggleTooth: (toothId: number) => void;
toggleBridge: (fromId: number, toId: number) => void;
selectUpper: () => void;
selectLower: () => void;
selectRange: (teethIds: number[]) => void;
clearAll: () => void;
isSelected: (toothId: number) => boolean;
hasBridge: (fromId: number, toId: number) => boolean;
getSelectedTeethData: () => Tooth[];
}TeethChart
SVG component for rendering the interactive tooth chart. Must be used inside TeethSelector.
| Prop | Type | Default | Description |
| --------------------- | ---------- | ------- | ---------------------- |
| width | number | 300 | SVG width |
| height | number | 500 | SVG height |
| className | string | - | Additional class names |
| enableDragSelection | boolean | true | Enable drag to select |
| renderTooth | function | - | Custom tooth renderer |
| renderBridge | function | - | Custom bridge renderer |
TeethPreview
Read-only component for displaying a teeth selection string.
| Prop | Type | Default | Description |
| -------------- | ---------- | ------- | --------------------------------------- |
| teeth | string | - | Selection string (e.g., "11, [21, 23]") |
| width | number | 150 | SVG width |
| height | number | 250 | SVG height |
| className | string | - | Additional class names |
| renderTooth | function | - | Custom tooth renderer |
| renderBridge | function | - | Custom bridge renderer |
Utilities
import {
teethData, // All 32 teeth with metadata
adjacentTeethPairs, // Valid bridge pairs
getTeethInRange, // Get teeth between two IDs
parseTeethSelection, // Parse string to selection
formatTeethSelection, // Format selection to string
} from "@salloomd/teeth-selector";Tooth Data Structure
interface Tooth {
order: number; // Display order (0-31)
id: number; // FDI tooth number (11-48)
name: string; // Full name
position: "upper" | "lower";
side: "left" | "right";
d: string; // SVG path data
}Dental Numbering (FDI)
The component uses FDI World Dental Federation notation:
- Upper Right: 18-11 (third molar to central incisor)
- Upper Left: 21-28 (central incisor to third molar)
- Lower Left: 38-31 (third molar to central incisor)
- Lower Right: 41-48 (central incisor to third molar)
License
MIT
