@pitchkit/react
Declarative React components for football pitch visualisation — mplsoccer's feature set for the web. Responsive by default, themed with CSS variables, SSR-safe.
Docs and live examples: pitchkitjs.com
Early days — pre-
1.0. Usable and tested, but the API isn't stable yet; expect breaking changes before1.0.
Install
npm install @pitchkit/reactReact >= 18 is a peer dependency. @pitchkit/core
is installed automatically.
Usage
import { Pitch, Scatter } from "@pitchkit/react";
const shots = [
{ x: 112, y: 39, xg: 0.76, outcome: "goal" },
{ x: 105, y: 44, xg: 0.31, outcome: "saved" },
{ x: 99, y: 47, xg: 0.13, outcome: "off target" },
];
export function ShotMap() {
return (
<Pitch type="statsbomb">
<Scatter
data={shots}
x={(s) => s.x}
y={(s) => s.y}
r={(s) => 3 + s.xg * 9}
fill={(s) => (s.outcome === "goal" ? "#fb923c" : "#38bdf8")}
tooltip={(s) => `${s.outcome} · xG ${s.xg.toFixed(2)}`}
/>
</Pitch>
);
}<Pitch> owns the coordinate system; children are layers drawn into it, stacked in render
order. Every visual prop accepts a static value or a function of the datum — fill="red"
and fill={(d) => d.teamColor} are the same prop.
Components
| Component | Draws |
|---|---|
<Pitch> | The pitch surface + coordinate context (horizontal) |
<VerticalPitch> | Same, rotated to a vertical framing |
<Scatter> | Circles — shots, players, events |
<Annotate> | Text labels |
<Arrows> | Straight arrows — passes, carries |
<Comet> | Tapered lines with direction implied by width |
<Heatmap> | Binned density on Canvas (client-only) |
<PositionalHeatmap> | Juego de Posición zone density on Canvas (client-only) |
<Hexbin> | Hexagonal density on Canvas (client-only) |
<KDE> | Smooth kernel density surface on Canvas (client-only) |
<Polygon> | Arbitrary closed shapes |
<ConvexHull> | Convex hull of a point set |
<Voronoi> | Voronoi cells, clipped to the pitch |
<GoalAngle> | The angle-to-goal wedge from a shot location |
<Flow> | Binned direction + magnitude vectors |
<PlayerAvatar> | Lineup avatar: photo, name, cards, subs, goals, assists |
usePitch() | Hook exposing the pixel transform for custom SVG |
<RaceChart> | Cumulative step chart over match minutes — the xG race chart |
useRaceChart() | Hook exposing a <RaceChart>'s scales, for annotations |
<MomentumChart> | Match momentum bars per half, with an event icon row |
useMomentumChart() | Hook exposing a <MomentumChart>'s scales, for annotations |
<RadarChart> | Player radar: per-axis ranges, flips, click-to-detail labels |
useRadarChart() | Hook exposing a <RadarChart>'s geometry, for annotations |
<PizzaChart> | Percentile pizza: group colours, click-to-detail slices |
usePizzaChart() | Hook exposing a <PizzaChart>'s geometry, for annotations |
<RaceChart>, <MomentumChart>, <RadarChart> and <PizzaChart> are the exception to "children are layers drawn into a <Pitch>": they have no pitch,
so each is a root in its own right and takes no type prop. See
Charts.
Sizing
Responsive is the default — with no size props the pitch fills its container via
ResizeObserver and recomputes on resize. Explicit sizing is the opt-out:
<Pitch type="statsbomb" /> {/* fills container */}
<Pitch type="statsbomb" width={1200} height={800} /> {/* fixed — exports, OG images */}Layer order
Markings paint below the layer children by default, so discrete marks sit on top of the
lines. An opaque density fill will therefore cover them — set appearance.linesOnTop to paint
the markings above instead (mplsoccer's line_zorder):
<Pitch type="statsbomb" appearance={{ linesOnTop: true }}>
<KDE data={touches} x={(t) => t.x} y={(t) => t.y} />
</Pitch>Only the markings move — the grass surface and stripes stay at the bottom either way.
Pitch types
statsbomb · opta · uefa, each using the provider's real coordinate space so event data
goes in unmodified.
import { cropForHalf, getPitchDimensions } from "@pitchkit/core";
const dimensions = getPitchDimensions("statsbomb");
<VerticalPitch type="statsbomb" crop={cropForHalf(dimensions)}>
{/* … */}
</VerticalPitch>;Theming
CSS variables only — no theme objects, no providers. Define once; every chart inherits, including dark mode:
:root {
--pitch-surface: #1a472a;
--pitch-stripe: #1d4f30;
--pitch-lines: rgba(255, 255, 255, 0.8);
}Marks accept className, so Tailwind works directly:
<Scatter data={shots} x={(s) => s.x} y={(s) => s.y} className="fill-emerald-400 stroke-white" />Setting className without an explicit fill/stroke makes the mark drop its themed default
so your class wins. For marks whose JSX you don't own, every element carries
data-pitchkit-mark / -layer / -part attributes to target instead.
Next.js / SSR
SVG marks server-render cleanly. Because layer components take accessor functions as props,
the <Pitch> tree must originate inside a "use client" component — React Server Components
can't pass functions across the client boundary. SSR still happens; only the prop-serialisation
boundary moves. <Heatmap> is Canvas-backed and therefore client-only.
Agent Skill
No model has PitchKit in its training data, so coding agents asked for a shot map tend to
invent an mplsoccer-flavoured API. This package ships an Agent Skill — skills/pitchkit/
inside the tarball — that documents the real one.
npx @pitchkit/react skills install # -> .claude/skills/pitchkit/
npx @pitchkit/react skills install --dir .cursor/skills
npx @pitchkit/react skills path # where it lives in node_modulesInstall symlinks the target at the copy inside node_modules, so npm update @pitchkit/react moves the skill with it and an agent can't end up reading last version's
API. On a filesystem that won't take a symlink it copies instead and says so — that copy is
a snapshot, so re-run with --force after upgrading.
The default directory suits Claude Code; for any other agent, point --dir at wherever it
reads skills from, or hand it the skills path output to read directly. The layout follows
the skills/<name>/SKILL.md convention, so generic installers like skills-npm find it in
node_modules without needing this CLI at all.
Links
- Documentation & gallery
- Repository
- Issues
- X · Instagram
@pitchkit/core— the framework-agnostic engine
Licence
MIT © Yohahn Ribeiro
Interfaces
- AnnotateProps
- ArrowsProps
- CometProps
- ConvexHullProps
- FlowProps
- GoalAngleProps
- GoalPoint
- GoalShotsProps
- GoalViewAppearance
- GoalViewContextValue
- GoalViewProps
- HeatmapProps
- HexbinProps
- KDEProps
- MomentumAppearance
- MomentumChartContextValue
- MomentumHover
- MomentumTeams
- PitchProps
- PizzaAppearance
- PizzaChartContextValue
- PizzaChartProps
- PizzaDetailContext
- PizzaGroup
- PizzaMetric
- PizzaSelection
- PizzaSeries
- PlayerAvatarProps
- PolygonProps
- PositionalHeatmapProps
- RaceAppearance
- RaceChartContextValue
- RaceChartProps
- RaceHoverRow
- RaceSeries
- RadarAppearance
- RadarChartContextValue
- RadarChartProps
- RadarDetailContext
- RadarMetric
- RadarSelection
- RadarSeries
- ResolvedRaceSeries
- ScatterProps
- TooltipState
- VoronoiProps
Type Aliases
- GoalFrameId
- GoalMarkerUnits
- LabelRotation
- MomentumChartProps
- MomentumEventKind
- MomentumSide
- PizzaSeriesLayout
- PlayerCard
- PlayerSubstitution