@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.json

To 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-tree

Then 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

PropTypeDescription
peoplePerson[]The family. The only required prop.
focusId / defaultFocusIdstring | nullWho the tree is drawn relative to. Controlled or uncontrolled.
onFocusChange(id) => voidFires when a card is selected.
focusMode"highlight" | "isolate"Dim everyone off the line, or filter them out and re-lay out.
renderCard(ctx) => ReactNodeReplace the card entirely. Receives person, relationship, branch and focus state.
formatRelationship(rel, person) => stringRewrite the calculated term, for translation or house style.
layoutPartial<LayoutOptions>Card size and the gaps between cards, rows and siblings.
showToolbar / showGenerationRulesbooleanBoth 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.

SituationTerm
Shares both parentsbrother / sister
Shares one parenthalf-brother / half-sister
Parent's spouse who is not a parentstep-mother / step-father
Grandparent's siblinggreat-aunt / great-uncle
Common ancestor two up on both sidesfirst cousin
Common ancestor two up, three on theirsfirst cousin once removed
Spouse's parentmother-in-law / father-in-law
Sibling's spousebrother-in-law / sister-in-law
Dissolved partnershipex-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.