Theme reference
Every token, its type, its default, and what reads it. Customization is the guide; this is the table you check when a value does not do what you expected.
The token names are identical on web and React Native. Four of them behave differently on the two platforms, and those rows say so.
Merging
defaultTheme ← provider theme ← tour theme ← step themeLowest to highest. Merging is key by key and one level deep into each group, so
{ scrim: { opacity: 0.5 } } keeps the default scrim.color.
<TourProvider theme={{ accent: "#ff0066" }} tours={[
{ id: "onboarding", version: 1, theme: { scrim: { opacity: 0.7 } }, steps: [
{ id: "fab", target: "fab", theme: { ring: { show: true } } },
]},
]}>mergeTheme(...overrides) is exported if you need the resolved object yourself. It applies
text.contrast last, so what it returns is what gets rendered.
Top level
| Token | Type | Default | Effect |
|---|---|---|---|
accent |
string |
#1E9CFE |
The Next button, and the fallback for progress.activeColor, progress.restColor and ring.color. |
zIndex |
number |
10000 |
Stacking order of the overlay root. High enough to clear modals, sticky headers and toasts. Lower it to put the tour under a fixed header. |
Without a z-index above your own layers, the scrim renders behind anything positioned above 0, and that element stays clickable during the tour.
scrim
The dimmed area outside the hole.
| Token | Type | Default | Effect |
|---|---|---|---|
scrim.color |
string |
#0B121E |
|
scrim.opacity |
number |
0.86 |
0 hides the dimming and keeps the cutout geometry. |
spotlight
The hole itself.
| Token | Type | Default | Effect |
|---|---|---|---|
spotlight.padding |
number |
4 |
Space between the target's box and the edge of the hole. A step's padding overrides it. |
spotlight.radius |
number | "auto" |
"auto" |
Corner radius of the hole. |
"auto" is the one token that resolves differently per platform:
| Platform | What "auto" resolves to |
|---|---|
| React Native | The radius prop on the TourTarget, or 0 when it has none. |
| Web | 8. There is no element radius to read, because a data-tour-id carries no geometry. |
So on web, set spotlight.radius to a number, or set radius per step. A circular hole around a
floating action button is radius: 999; a tight list row is radius: 8, padding: 2.
On web the padding is added to the radius, so the hole's corners stay concentric with the target's as the padding grows.
card
| Token | Type | Default | Effect |
|---|---|---|---|
card.background |
string |
#FBFCFE |
Also the arrow's fill, and the input to text.contrast: "auto". |
card.radius |
number |
16 |
|
card.padding |
number |
16 |
|
card.maxWidth |
number |
320 |
The card is max-content up to this width. |
card.shadow |
"none" | "lifted" |
"lifted" |
Web draws 0 4px 10px rgba(17, 24, 39, 0.15). React Native draws the iOS shadow plus elevation: 5. |
text
Three styles and a switch. Each style is { fontSize, fontWeight, color }.
| Token | Type | Default |
|---|---|---|
text.title.fontSize |
number |
15 |
text.title.fontWeight |
FontWeight |
"700" |
text.title.color |
string |
#111827 |
text.body.fontSize |
number |
13 |
text.body.fontWeight |
FontWeight |
"500" |
text.body.color |
string |
#6B7280 |
text.action.fontSize |
number |
13 |
text.action.fontWeight |
FontWeight |
"700" |
text.action.color |
string |
#1E9CFE |
text.contrast |
"manual" | "auto" |
"manual" |
FontWeight is "normal", "bold", or "100" through "900". Strings, not numbers, because
React Native rejects numeric weights.
The default card renders the action colour as accent, not text.action.color. Set accent
to change the Next button. text.action is what a custom card should read.
text.contrast
"manual" leaves the three colours exactly as set. "auto" derives title and body from
card.background by contrast ratio:
| What | How it is chosen |
|---|---|
| Title and body | Whichever of the light pair (#F9FAFB / #CBD5E1) or the dark pair (#111827 / #6B7280) scores a higher ratio against card.background. |
| Action | accent, when it clears 3:1 against card.background. Otherwise the chosen title colour. |
Higher ratio, not darker. On #1E9CFE, dark text scores 7.4:1 against white text's 2.7:1, so dark
text wins.
theme={{ card: { background: "#101828" }, text: { contrast: "auto" } }}This is the token to reach for when your card background is user-configurable or follows a dark
mode toggle. contrastRatio, luminance and isDark are exported from @tourkit/core if a
custom card needs the same answers.
arrow
| Token | Type | Default | Effect |
|---|---|---|---|
arrow.size |
number |
14 |
The square that gets rotated 45 degrees. |
arrow.show |
boolean |
true |
false removes it. A step with no target never has one. |
motion
| Token | Type | Default | Effect |
|---|---|---|---|
motion.morph |
number |
280 |
Milliseconds for the hole to travel and resize between steps. |
motion.travel |
number |
200 |
Milliseconds for the card to move. |
motion.fade |
number |
180 |
Milliseconds for the card's fade. |
motion.easing |
string |
"easeOutQuint" |
React Native only. easeOutQuint, easeOut, linear. An unknown value falls back to easeOutQuint. |
Set any duration to 0 to turn that animation off.
motion.easing has no effect on web. The web host animates on a fixed
cubic-bezier(0.22, 1, 0.36, 1), which is the same curve as easeOutQuint. Changing the token
in a shared theme is harmless; expecting linear to land in the browser is not.
Reduced motion is read from the platform on both sides. Web watches
prefers-reduced-motion: reduce and re-reads it on change. React Native reads
AccessibilityInfo. Either one switches the hole from animating to jumping and disables the ring.
progress
| Token | Type | Default | Effect |
|---|---|---|---|
progress.style |
ProgressStyle |
"dots" |
See below. |
progress.activeColor |
string | null |
null |
Falls back to accent. |
progress.restColor |
string | null |
null |
Falls back to accent. |
progress.style |
What it draws |
|---|---|
dots |
One dot per step. |
segmented |
One bar per step; the active one stretches into a rounded rectangle. |
numbers |
3 / 7, in tabular figures. |
continuous |
A single track that fills as the tour advances. |
All four render on both platforms.
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.
| Token | Type | Default | Effect |
|---|---|---|---|
ring.show |
boolean |
false |
|
ring.color |
string | null |
null |
Falls back to accent. |
ring.width |
number |
2 |
|
ring.period |
number |
1400 |
Milliseconds for one breath. |
Reduced motion is handled differently on the two platforms. Web does not render the ring at all. React Native still draws it, at a constant 30% opacity, and skips the breathing animation. Neither one leaves a half-finished animation frozen on screen.
blur
| Token | Type | Default | Effect |
|---|---|---|---|
blur.enabled |
boolean |
false |
|
blur.radius |
number |
7 |
Pixels. |
Web applies backdrop-filter: blur(Npx) to the scrim, leaving the cutout unblurred. Nothing to
install.
React Native needs @react-native-masked-view/masked-view and expo-blur, plus a rebuild, and
you hand the modules to createBlurBackdrop yourself. Turning blur.enabled on without mounting
that backdrop keeps the dim scrim and logs one development warning naming what to install. The
reason the package does not probe for the modules itself is in
Customization.
The full object
Every field is optional. This is the resolved default, which is what defaultTheme exports.
const defaultTheme = {
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 },
};Following a dark mode toggle
The theme prop can change while a tour is running and the engine follows without restarting.
const theme = useMemo(
() =>
dark
? { card: { background: "#101828" }, scrim: { opacity: 0.9 }, text: { contrast: "auto" } }
: { text: { contrast: "auto" } },
[dark],
);
<TourProvider tours={tours} theme={theme}>Memoize it. A fresh object every render pushes new options into the engine on every render.
When the theme is not enough
The theme covers colour, size and timing. It does not cover layout or markup. For those:
- Web: the stable classes, or your own through
classNames. See Customization. - Either platform: replace the
Card,BackdroporProgressslot. - Either platform:
useTourState()and render the whole thing yourself.