PitchKit
Configuration

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

VariableControlsDefault
--pitch-surfaceGrass fill#1a472a
--pitch-stripeMow-stripe overlayrgba(255, 255, 255, 0.04)
--pitch-linesPitch markings (lines, circles, boxes)rgba(255, 255, 255, 0.8)
--pitch-line-widthPitch marking stroke width1.5
--pitch-marker-primaryDefault 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.

On this page