@familytree/family-tree
Documentation
A vertical family tree component. Everything below ships as source into your project.
Installation
The component and its whole engine install as one registry item. It pulls in the shadcn primitives it composes — card, badge, button, separator, toggle-group and tooltip — and has no runtime dependencies of its own.
npx shadcn@latest add https://shadcn-family-tree.ryck.dev/r/family-tree.jsonTo install by name instead, register the namespace once in your components.json:
{
"registries": {
"@familytree": "https://shadcn-family-tree.ryck.dev/r/{name}.json"
}
}npx shadcn@latest add @familytree/family-treeThen render it inside something with a height.
import { FamilyTree } from "@/components/ui/family-tree"
export function Page() {
return (
<div className="h-svh">
<FamilyTree people={people} defaultFocusId="iris" />
</div>
)
}Data model
A person needs a name and a sex. Relationships are flat: point at parents and partners by id, and the component derives the couples — what genealogists call unions — from that.
const people: Person[] = [
{ id: "a", name: "Ada Vance", sex: "female" },
{ id: "s", name: "Sam Vance", sex: "male", partnerIds: ["a"] },
{ id: "i", name: "Iris Vance", sex: "female", parentIds: ["a", "s"] },
]Every distinct pair of parents becomes its own union, so half siblings separate automatically — no extra bookkeeping. Use partnerships instead of partnerIds when you want a status or a date, which is what drives the connector styling: solid for a marriage, dashed for a partnership, hatched for a divorce.
{
id: "felix",
name: "Felix Marchetti",
sex: "male",
partnerships: [
{ partnerId: "edith", status: "divorced", since: "1970", until: "1980" },
{ partnerId: "nadia", status: "married", since: "1983" },
],
}Everything else is optional and rendered when present: nickname, birth, death, location, branchId to override the computed colour, and meta for anything you want to reach in a custom card. A location renders as a link to Google Maps.
Malformed data produces console diagnostics in development rather than exceptions — unknown ids, more than two parents and ancestry cycles are all reported and then worked around.
Props
| Prop | Type | Description |
|---|---|---|
| people | Person[] | The family. The only required prop. |
| focusId / defaultFocusId | string | null | Who the tree is drawn relative to. Controlled or uncontrolled. |
| onFocusChange | (id) => void | Fires when a card is selected. |
| focusMode | "highlight" | "isolate" | Dim everyone off the line, or filter them out and re-lay out. |
| renderCard | (ctx) => ReactNode | Replace the card entirely. Receives person, relationship, branch and focus state. |
| formatRelationship | (rel, person) => string | Rewrite the calculated term, for translation or house style. |
| layout | Partial<LayoutOptions> | Card size and the gaps between cards, rows and siblings. |
| showToolbar / showGenerationRules | boolean | Both default to true. |
Relationship terms
Selecting a person recomputes every other card's badge against them. The calculation walks both pedigrees to the nearest common ancestor and names the result, so it handles the awkward cases rather than stopping at cousins.
| Situation | Term |
|---|---|
| Shares both parents | brother / sister |
| Shares one parent | half-brother / half-sister |
| Parent's spouse who is not a parent | step-mother / step-father |
| Grandparent's sibling | great-aunt / great-uncle |
| Common ancestor two up on both sides | first cousin |
| Common ancestor two up, three on theirs | first cousin once removed |
| Spouse's parent | mother-in-law / father-in-law |
| Sibling's spouse | brother-in-law / sister-in-law |
| Dissolved partnership | ex-husband / ex-wife |
Terms follow the person being described. A sex of other or unknown falls back to neutral wording — sibling, child, partner — rather than guessing.
Theming
Sizing and colour come from CSS custom properties on the root element, so you can restyle without touching the source.
.my-tree {
--ft-card-width: 240px;
--ft-card-height: 148px;
--ft-row-gap: 96px;
--ft-connector-width: 2px;
--ft-dim-opacity: 0.2;
--ft-branch-1: oklch(0.55 0.16 255);
}Changing --ft-card-width or --ft-card-height alone only restyles the cards. The layout is computed in JavaScript, so pass the same numbers through the layout prop for the geometry to follow.
Cards and connectors carry stable data attributes — data-person-id, data-sex, data-generation, data-branch, data-focused, data-deceased, data-dimmed — so you can hook styles onto any of them.
Accessibility
Cards are real buttons with an accessible name that includes the calculated relationship, so a screen reader announces "Colin Hollis, father" rather than just a name. The viewport is described by a summary of how many people and generations it holds.
Arrow keys navigate by family rather than by document order: up and down move to a parent or a child, left and right step along the current generation, and keyboard focus follows the selection. Pan and zoom animations respect prefers-reduced-motion.