Tailwind
Theming with CSS variables, and four ways to reach PitchKit with Tailwind utilities.
PitchKit's theming is CSS variables only — no JS theme objects, no per-instance color
props to thread through. Set a variable once in your global stylesheet and every <Pitch>
picks it up through the CSS cascade.
The variables
| Variable | Controls | Default |
|---|---|---|
--pitch-surface | Grass fill | #1a472a |
--pitch-stripe | Mow-stripe overlay | rgba(255, 255, 255, 0.04) |
--pitch-lines | Pitch markings (lines, circles, boxes) | rgba(255, 255, 255, 0.8) |
--pitch-line-width | Pitch marking stroke width | 1.5 |
--pitch-marker-primary | Default mark color (<Scatter>, <Arrows>, <Comet>) | #3b82f6 |
/* globals.css */
:root {
--pitch-surface: #1a472a;
--pitch-stripe: rgba(255, 255, 255, 0.05);
--pitch-lines: rgba(255, 255, 255, 0.8);
--pitch-marker-primary: #3b82f6;
}
This alone works with or without Tailwind in the picture — dark mode is a second :root
override behind a class or media query, and a one-off chart can override any variable on a
wrapper <div>. What follows is specifically about reaching those variables (and individual
marks) with Tailwind utilities, once it's in the project.
Marks you render yourself: className
<Scatter>, <Arrows>, and <Comet> fall back to a themed default fill/stroke — but
that default is applied as inline style, and inline style always beats a class at the same
CSS property. So a className utility only takes effect once the corresponding accessor prop
is omitted entirely — passing both fill="red" and a fill-* class silently drops the
class.
"use client";import { Comet, Pitch, Scatter } from "@pitchkit/react";import { docsAppearance } from "./docs-appearance";// StatsBomb coordinates (120 x 80).const run = { from: { x: 30, y: 20 }, to: { x: 70, y: 55 } };const shots = [ { x: 95, y: 35 }, { x: 101, y: 42 }, { x: 108, y: 38 },];/** * `className` reaches a mark you render yourself — but only once the * accessor prop it would otherwise fill (`fill`/`stroke`/`color`) is * omitted. That prop is applied as inline style, and inline style always * beats a class at the same property. */export function TailwindClassnameBasic() { return ( <Pitch type="statsbomb" appearance={docsAppearance}> <Comet data={[run]} x={(d) => d.from.x} y={(d) => d.from.y} x2={(d) => d.to.x} y2={(d) => d.to.y} className="fill-fuchsia-400" /> <Scatter data={shots} x={(d) => d.x} y={(d) => d.y} r={6} strokeWidth={1.5} className="fill-cyan-300 stroke-white transition-colors hover:fill-cyan-100" /> </Pitch> );}Marks you don't own: a data-pitchkit-* selector
Sometimes you're styling a mark whose JSX isn't yours — inside a wrapped recipe component, for
example. Every mark already carries data-pitchkit-mark/data-pitchkit-layer attributes with
zero setup, reachable via Tailwind's arbitrary-variant selectors on a wrapping element:
"use client";import { Arrows, Pitch, Scatter } from "@pitchkit/react";import { docsAppearance } from "./docs-appearance";// StatsBomb coordinates (120 x 80).const pass = { from: { x: 40, y: 60 }, to: { x: 85, y: 30 } };const shots = [ { x: 95, y: 35 }, { x: 101, y: 42 }, { x: 108, y: 38 },];/** * For a mark whose JSX you don't own (e.g. inside a shadcn recipe * wrapping `<Pitch>`), reach it via the `data-pitchkit-mark` attribute * every mark already carries — no `className` prop needed on the mark * itself. The trailing `!` is required: these marks never got a * `className` to signal an opt-out, so their themed default is still an * inline style, which only an `!important` utility can out-rank. */export function TailwindAttributeBasic() { return ( <div className="[&_[data-pitchkit-mark=arrow-head]]:fill-amber-400! [&_[data-pitchkit-mark=arrow-shaft]]:stroke-amber-400! [&_[data-pitchkit-mark=scatter]]:fill-amber-200!"> <Pitch type="statsbomb" appearance={docsAppearance}> <Arrows data={[pass]} x={(d) => d.from.x} y={(d) => d.from.y} x2={(d) => d.to.x} y2={(d) => d.to.y} /> <Scatter data={shots} x={(d) => d.x} y={(d) => d.y} r={6} /> </Pitch> </div> );}Note the trailing ! (Tailwind v4's important modifier) in the class list above — it's
required here specifically. Unlike the className mechanism, these marks never got a
className to signal an opt-out from their themed default, so an ordinary class can't
out-rank the inline style they still carry.
The pitch background: a @utility recipe
The pitch background (outline, stripes, lines) isn't a mark at all — it's restyled only
through the --pitch-* variables above, never via className on the shapes directly. Two
ways to set them with Tailwind:
Zero setup, using Tailwind's arbitrary-property syntax:
<Pitch type="statsbomb" className="[--pitch-surface:var(--color-emerald-950)]" />
Or install this recipe once (a "shadcn add"-style snippet — copy it into your own
globals.css) to get autocompletable, first-class utilities instead, reading any color
already in your Tailwind theme:
/* globals.css */
@utility pitch-surface-* {
--pitch-surface: --value(--color-*);
}
@utility pitch-stripe-* {
--pitch-stripe: --value(--color-*);
}
@utility pitch-lines-* {
--pitch-lines: --value(--color-*);
}
"use client";import { Pitch } from "@pitchkit/react";import { docsAppearance } from "./docs-appearance";/** * The pitch background (outline/stripes/lines) isn't a mark — it's * restyled only via `--pitch-surface`/`--pitch-stripe`/`--pitch-lines`. * `pitch-surface-*`/`pitch-stripe-*`/`pitch-lines-*` are custom Tailwind * utilities (an installable `@utility ... --value(--color-*)` recipe, see * the Configuration > Tailwind page) that turn those variables into * first-class classes reading any color already in the project's theme. */export function TailwindRecipeBasic() { return ( <Pitch type="statsbomb" appearance={docsAppearance} className="pitch-surface-emerald-950 pitch-stripe-emerald-800 pitch-lines-amber-500" /> );}Line width
--pitch-line-width gets the same recipe shape, but validates a bare/arbitrary number instead
of looking one up in the theme — Tailwind has no rich preset scale for stroke width the way it
does for color:
@utility pitch-line-width-* {
--pitch-line-width: --value(number, [number]);
}
"use client";import { Pitch } from "@pitchkit/react";import { docsAppearance } from "./docs-appearance";/** * `--pitch-line-width` gets the same recipe shape as the color utilities, * but validates a bare/arbitrary number instead of looking one up in the * theme — Tailwind has no rich preset scale for stroke width to borrow * from the way it does for color. */export function TailwindLineWidthBasic() { return ( <Pitch type="statsbomb" appearance={docsAppearance} className="pitch-line-width-4 pitch-lines-amber-600 pitch-surface-black pitch-stripe-gray-800" /> );}pitch-line-width-4 (bare) and [--pitch-line-width:4] (zero-setup arbitrary property) are
equally reasonable here — pick whichever reads better in context.