Customization
A tourkit overlay can be customized at three levels: theme tokens, component slots, and fully headless rendering. Most apps stop at the first. The third exists so nobody has to fork the package.
Level 1: theme tokens
One object. Set it on the provider, override it per tour, override it again per step. The token names are identical on web and React Native.
const theme = {
accent: "#1E9CFE",
zIndex: 10000,
scrim: { color: "#0B121E", opacity: 0.86 },
spotlight: { padding: 4, radius: "auto" },
card: { background: "#FBFCFE", radius: 16, padding: 16, maxWidth: 320, shadow: "lifted" },
text: {
title: { fontSize: 15, fontWeight: "700", color: "#111827" },
body: { fontSize: 13, fontWeight: "500", color: "#6B7280" },
action: { fontSize: 13, fontWeight: "700", color: "#1E9CFE" },
contrast: "manual",
},
arrow: { size: 14, show: true },
motion: { morph: 280, travel: 200, fade: 180, easing: "easeOutQuint" },
progress: { style: "dots", activeColor: null, restColor: null },
ring: { show: false, color: null, width: 2, period: 1400 },
blur: { enabled: false, radius: 7 },
};activeColor, restColor and ring.color fall back to accent when left null.
Everything is optional and merges key by key, so { scrim: { opacity: 0.5 } } keeps the default
scrim colour.
Precedence, lowest to highest: defaults, provider, tour, step.
<TourProvider theme={{ accent: "#ff0066" }} tours={[
{ id: "onboarding", version: 1, theme: { scrim: { opacity: 0.7 } }, steps: [
{ id: "fab", target: "fab", radius: 999, padding: 12 },
{ id: "row", target: "row", radius: 8, padding: 2 },
]},
]}>Per-step radius and padding matter more than they look. A floating action button wants a
circular hole with generous padding; the list row after it wants a tight rectangle.
Progress
progress.style |
What it draws |
|---|---|
dots |
One dot per step. The default. |
segmented |
One bar per step; the active one stretches into a rounded rectangle. |
numbers |
3 / 7, tabular figures. |
continuous |
A single track that fills as the tour advances. |
theme={{ progress: { style: "segmented", activeColor: "#1E9CFE", restColor: "#94A3B8" } }}Text that follows the card colour
text.contrast defaults to manual, which leaves the three text colours exactly as set. Switch it
to auto and the title and body are derived from card.background by contrast ratio, picking
whichever of light or dark text scores higher. The action keeps accent when that clears 3:1
against the card, and falls back to the title colour when it does not.
theme={{ card: { background: "#101828" }, text: { contrast: "auto" } }}A mid-tone card gets whichever text reads better, not whichever is darker: on #1E9CFE, dark text
scores 7.4:1 against white text's 2.7:1, so dark text wins.
Ring
A breathing outline around the hole, off by default because switching a looping animation on for every existing tour is a change nobody asked for.
theme={{ ring: { show: true, color: "#1E9CFE", width: 2, period: 1400 } }}Reduced motion disables it on both platforms.
Blur
theme={{ blur: { enabled: true, radius: 7 } }}Web uses backdrop-filter on the scrim and needs nothing installed.
React Native needs @react-native-masked-view/masked-view and expo-blur, plus a rebuild. Install
both, then hand them to the backdrop yourself:
import MaskedView from "@react-native-masked-view/masked-view";
import { BlurView } from "expo-blur";
import { createBlurBackdrop, TourProvider } from "@tourkit/native";
const Backdrop = createBlurBackdrop({ MaskedView, BlurView });
<TourProvider tours={tours} theme={{ blur: { enabled: true } }} components={{ Backdrop }}>You pass the modules rather than the library importing them, because a library that probes for an
optional module with require reports a missing-module error to React Native's global error
handler, and an app with its own handler treats that as fatal and blanks the screen. Turning
blur.enabled on without mounting the blur backdrop keeps the dim scrim and logs one warning in
development naming what to install, never a blank overlay.
zIndex
Defaults to 10000, which puts the tour above modals, sticky headers and toasts. Lower it if you want the tour to sit under a fixed header. Without a z-index the scrim renders behind anything positioned above 0, and that element stays clickable during the tour.
Set any motion duration to 0 to turn that animation off. Reduced motion is honoured
automatically on both platforms.
easing accepts easeOutQuint, easeOut or linear, and applies on React Native only. The web
host animates on a fixed curve identical to easeOutQuint. Every token, with its type, its
default and the four that differ per platform, is in the Theme reference.
Level 2: component slots
Replace a piece, keep the engine. Every slot gets typed props.
<TourProvider components={{ Card, Backdrop, Progress }} tours={tours}>type CardProps<Ctx = unknown> = {
step: TourStep<Ctx>;
index: number;
total: number;
rect: Rect | null;
placement: CardPlacement;
theme: Theme;
isFirst: boolean;
isLast: boolean;
next(): void;
prev(): void;
skip(): void;
stop(): void;
};Web cards also receive styled: boolean.
The host positions the card for you. Your component renders the contents and, if it wants one,
an arrow using placement.arrow.
function Card({ step, index, total, next, isLast }: CardProps) {
return (
<div className="rounded-xl bg-white p-4 shadow-lg">
<h3 className="font-semibold">{step.title}</h3>
<p className="text-sm text-gray-500">{step.body}</p>
<div className="mt-3 flex justify-between">
<span className="text-xs">{index + 1} of {total}</span>
<button onClick={next}>{isLast ? "Done" : "Next"}</button>
</div>
</div>
);
}On React Native, Backdrop also receives size, the measured box the overlay actually covers.
Use it rather than useWindowDimensions(): under Android edge-to-edge the window it reports is
shorter than the screen by the system bars, and a scrim drawn to that height leaves a live strip
along the bottom.
step.data is an arbitrary object the library never reads. Put an image URL, a video, a CTA or a
translation key in it and pull it out in your card.
Web: your own CSS
Every element carries a stable class as well as its data attribute, so an external stylesheet or a Tailwind layer can target the overlay without going through the theme object.
| Class | Element |
|---|---|
tourkit-root |
The portalled wrapper |
tourkit-overlay |
The scrim with the cutout |
tourkit-shield |
The click blocker |
tourkit-card, tourkit-arrow |
The coach card and its beak |
tourkit-title, tourkit-body, tourkit-next |
Inside the card |
tourkit-progress, tourkit-progress-step |
The progress indicator |
tourkit-ring |
The attention ring, when enabled |
Add your own on top through the provider:
<TourProvider classNames={{ root: "my-tour", overlay: "my-scrim", card: "my-card" }} tours={tours}>Yours are appended, never substituted, so tourkit-card my-card is what lands in the DOM.
Level 3: headless
useTourState() hands you everything and renders nothing.
const { status, step, stepIndex, total, rect, theme, next, prev, skip, stop } = useTourState();You draw the overlay, the card and the highlight yourself. @tourkit/core ships no UI at all, so
this is also the path if you are building a renderer for another framework.
Web: unstyled mode
driver.js makes you override CSS classes. react-joyride makes you fill in a styles object. Both
fight a design system.
import { TourProvider } from "@tourkit/react/unstyled";Nothing changes structurally. position, inset, clip-path, left and top still apply,
because without them the overlay does not work. Every colour, font, radius, padding and shadow is
dropped.
Style it with your own CSS through the data attributes:
| Attribute | On |
|---|---|
data-tourkit="root" |
The portal root. Also carries data-tourkit-state and data-tourkit-interaction. |
data-tourkit="shield" |
The full-screen blocker. Absent in passthrough. |
data-tourkit="backdrop" |
The clipped scrim. |
data-tourkit="card-wrap" |
The positioned wrapper. Carries data-tourkit-placement. |
data-tourkit="card" |
The card itself. |
data-tourkit="arrow" |
The arrow. |
data-tourkit="title", "body", "footer", "next" |
Card parts. |
data-tourkit="progress", "progress-dot" |
Progress. Dots carry data-tourkit-done. |
data-tourkit="hole-catcher" |
The press catcher in advance-on-press. |
[data-tourkit="card"] { @apply rounded-2xl bg-white p-5 shadow-xl; }
[data-tourkit="backdrop"] { background: rgb(0 0 0 / 0.6); }
[data-tourkit-placement="top"] [data-tourkit="arrow"] { background: white; }There is no stylesheet to import in either mode. A package that ships CSS breaks in several bundlers and in server rendering, so the default entry uses inline styles instead.