Styling & custom CSS
This page covers everything you can restyle today, in order of how much
control it gives you: the dashboard theme editors, --sk-* CSS variables,
data-sk-* targeting attributes, best-effort sk- class names, and — as a
last resort — project Custom CSS. Most teams never get past the first
section.
Theme editors first
Colors, typography, alignment, button radius, overlay, and progress display are all dashboard settings — no CSS needed. The tour and tooltip editors also have a Shape & spacing section (popover padding, corner radius, border width/color, shadow, max width) plus a font-family picker and a close-icon color override, and tours get an explicit footer-button padding control too. Each surface has its own project-wide default, with per-item overrides on top:
- Tours —
/app/<project>/theme/tours, with per-tour overrides in the tour builder. - Tooltips —
/app/<project>/theme/tooltips, with per-tooltip overrides. - Announcements —
/app/<project>/theme/announcements. - Surveys —
/app/<project>/theme/surveys.
Reach for the rest of this page only for what the editors don't expose yet — mainly the announcement and survey cards, whose padding and font family are CSS-variable-only today, and anything you'd rather set from your own stylesheet than the dashboard.
CSS variables (supported)
Set any of these as custom properties on your own page — typically on
:root — and StepsKit picks them up. Every variable falls back to its
current default, so setting nothing changes nothing:
| Variable | Applies to | Default | Theme setting | Notes |
|---|---|---|---|---|
--sk-popover-padding | Tour & tooltip popovers | 20px | Shape & spacing → Padding | Accepts any padding value, including the two- and four-value forms. |
--sk-popover-radius | Tour & tooltip popovers | 12px | Shape & spacing → Corner radius | |
--sk-popover-border-width | Tour & tooltip popovers | 0 (no border) | Shape & spacing → Border width | |
--sk-popover-border-color | Tour & tooltip popovers | currentColor | Shape & spacing → Border color | Only visible once the border width is set. |
--sk-popover-shadow | Tour & tooltip popovers | 0 1px 10px #0006 | Shape & spacing → Shadow | Takes a full box-shadow value; use none to remove it. |
--sk-popover-max-width | Tour & tooltip popovers | 300px | Shape & spacing → Max width | The 250px minimum width shrinks with it — see the browser note below. |
--sk-font-family | Every surface | the built-in Inter stack | Typography → Font | Banners and surveys let you override it on their own with --sk-banner-font-family / --sk-survey-font-family, which win when set. The tooltip indicator (.sk-tooltip-icon) reads it too, falling back to this same stack minus the two emoji families when it's unset. |
--sk-button-padding-x | Tour popovers only | 16px | Buttons → Horizontal padding | Horizontal padding on the tour popover's Back/Next buttons. A tooltip's CTA button sets its own padding inline from its size setting, which outranks this variable even when the tooltip's footer itself is visible. |
--sk-button-padding-y | Tour popovers only | 8px | Buttons → Vertical padding | Vertical padding on the tour popover's Back/Next buttons. Same override as --sk-button-padding-x — a tooltip's CTA sets its own padding inline. |
--sk-close-button-color | Tour popovers only | #6b7280 | Popover → Close icon | Also applies to the close button's hover/focus state, so setting it removes the default hover darkening. A tooltip popover always paints its own close color from its theme, so this variable has no effect there. |
--sk-banner-padding | Announcement banner | 10px 16px | — | Overrides both the desktop default and the narrow-screen default (10px 12px). No dashboard control yet. |
--sk-banner-font-family | Announcement banner | the system-font stack | — | Wins over --sk-font-family when both are set. No dashboard control yet. |
--sk-survey-padding | Survey card | 16px 18px | — | No dashboard control yet. |
--sk-survey-font-family | Survey card | the system-font stack | — | Wins over --sk-font-family when both are set. No dashboard control yet. |
--sk-survey-radius | Survey card | 12px | — | No dashboard control yet. |
:root {
--sk-popover-padding: 12px;
--sk-popover-radius: 8px;
--sk-popover-border-width: 1px;
--sk-popover-border-color: #e5e7eb;
--sk-popover-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
--sk-font-family: "Inter", system-ui, sans-serif;
--sk-button-padding-x: 12px;
--sk-button-padding-y: 6px;
}A few things worth knowing before you set these:
- Use
pxunits for lengths. Invalid values fall back to the default above rather than breaking the surface. :root { --sk-font-family: inherit }does not do what it looks like it does.inheritis a CSS-wide keyword that applies to the custom property itself, not to whatever later consumes it throughvar()— so the popover ends up inheriting nothing useful. To make StepsKit use your page's own font, write.sk-popover { font-family: inherit !important }in your stylesheet instead.- The 250px popover minimum width needs a modern browser. It's computed
with CSS
min(), which needs Chrome 79+, Safari 11.1+, or Firefox 75+. Everything else about these variables works everywhere; on an older browser you simply get no minimum width instead of a 250px floor.
These names are the supported customer styling surface. We won't rename them without a deprecation period.
How the cascade works
The general rule covers every variable in the table above: your :root
variable applies for as long as the matching dashboard setting is empty, and a
value you set in the theme editor wins once you set it. That holds for every
theme property, not only the ones with a --sk-* hook — a dashboard value is
applied after your stylesheet loads, most of them as an !important property
on the popover. Overlay color and opacity are the one exception: the embed
sets those as inline attributes on the overlay SVG rather than through an
!important rule, but they still take effect after your stylesheet either
way, and there's no --sk-overlay-* variable to set them from CSS at all. To
override a dashboard value from your own stylesheet, add !important at one
specificity level higher than the dashboard's own rule (a single class), for
example:
body .sk-popover {
background-color: #fff !important;
}That "one level higher" rule is for your own stylesheet. Custom CSS (below) sits later in the cascade than your stylesheet does, so a rule there only needs to match the dashboard rule's specificity, not exceed it — see Where Custom CSS sits in the cascade for the full ladder, including the exact selector for every theme key.
Five variables have no matching dashboard setting yet — --sk-banner-padding,
--sk-banner-font-family, --sk-survey-padding, --sk-survey-font-family,
and --sk-survey-radius — so those always apply uncontested; nothing on the
dashboard side can currently override them.
Per-step overrides made in the tour builder are applied as inline styles and
can't be overridden from your stylesheet at all — inline beats every
selector, !important or not.
Targeting one tour, step, tooltip, banner, or survey
Every rendered surface carries a data-sk-* attribute naming what it is, so
you can restyle one item instead of every popover on the page:
| Attribute | Where | Value |
|---|---|---|
data-sk-tour | Tour popover | The tour's slug. |
data-sk-tour-id | Tour popover | The tour's id — stable across renames (a slug is re-derived from the tour's name on every rename). |
data-sk-step | Tour popover | The step's 1-based position in the whole tour. This is not the same number as the popover's own progress text ("1 of 2"), which counts only the steps on the current page. |
data-sk-step-type | Tour popover | The step type (callout or modal). |
data-sk-tooltip | Tooltip indicator and tooltip popover | The tooltip's id. |
data-sk-announcement | Announcement banner | The announcement's id. |
data-sk-survey | Survey card | The survey's id. |
Combine an attribute with a CSS variable to restyle one tour without touching any other:
.sk-popover[data-sk-tour="welcome-tour"] {
--sk-popover-padding: 8px;
}Or target a class directly, for a per-step rule a variable can't express:
.sk-popover[data-sk-step="1"] .sk-popover-title {
text-transform: uppercase;
}Classes (best effort)
The class names below are real and tested, but the popover's internal markup can change as the product evolves — treat them as a looser contract than the CSS variables and attributes above.
Tour popover
sk-popover, sk-popover-arrow, sk-popover-close-btn,
sk-popover-title, sk-popover-description, sk-popover-image,
sk-popover-footer, sk-popover-progress-text, sk-popover-progress-dots,
sk-popover-progress-dot, sk-popover-navigation-btns,
sk-popover-prev-btn, sk-popover-next-btn, sk-overlay
Tooltip
sk-tooltip-popover (the popover), sk-tooltip-icon (the indicator),
sk-tooltip-cta-btn (its call-to-action button).
Modifiers (may change): one sk-tooltip-icon-* class per indicator style —
sk-tooltip-icon-info, sk-tooltip-icon-question, sk-tooltip-icon-dot,
sk-tooltip-icon-badge, sk-tooltip-icon-pulse.
Announcement banner
sk-banner, sk-banner-icon, sk-banner-content, sk-banner-title,
sk-banner-body, sk-banner-cta, sk-banner-dismiss
Survey card
sk-survey, sk-survey-question, sk-survey-scores, sk-survey-score,
sk-survey-textarea, sk-survey-actions, sk-survey-btn,
sk-survey-dismiss, sk-survey-thanks
Host page markers
These aren't StepsKit's own elements. They're classes StepsKit adds to your page while a tour runs, so your own stylesheet — or your Custom CSS, below — can react to one being active:
| Class | Where | Meaning |
|---|---|---|
sk-active | Your <body> | A tour is currently running. |
sk-fade | Your <body> | Set alongside sk-active when step transitions are animated (the default). |
sk-simple | Your <body> | Set alongside sk-active instead of sk-fade when animations are turned off. |
sk-active-element | The element the current step is highlighting | This is YOUR element — the one the tour is pointing at, not something StepsKit rendered. |
sk-no-interaction | The highlighted element | Set alongside sk-active-element only on a step that disables interaction with its target. |
All five clear automatically when the tour ends or moves to its next target — there's nothing to clean up from your side.
Documented classes are covered by tests, and we announce renames. Nesting and modifier classes not listed here may change without notice.
Custom CSS (last resort)
Once the theme editor, the --sk-* variables, and the classes and
attributes above still don't cover what you need, Theme → Custom CSS
(/app/<project>/theme/custom-css) is where you write an actual stylesheet.
It's one project-wide sheet, layered under the theme and applied to every
tour, tooltip, announcement banner, and survey — reach for it only for what
the tiers above can't express, since its rules aren't a versioned contract
the way the variables are.
Save it and every visitor picks it up within about 2 minutes — long enough for any cached copy of the tours payload to expire. No deploy, no rebuild.
You don't have to write it by hand: the editor's Ask your AI agent card
copies a prompt with the rules below and your current sheet, and an agent
connected to the StepsKit MCP writes and saves it directly
(update_project_custom_css).
The rules
Every selector has to anchor to something StepsKit actually renders or marks:
- A
.sk-class, anywhere in the selector, outside:not()and:has()—.sk-popover,.sk-banner-title,[data-sk-tour="welcome"] .sk-popover-title, and.sk-popover:has(.sk-popover-image)all anchor;:not(.sk-popover)anddiv:has(.sk-popover)don't, because the element they actually match isn't ours. - The host-page markers from the table above count as anchors too —
sk-active,sk-active-element, and the rest. - A bare
:root,html, orbodyrule — but only when every declaration in it sets a--sk-*custom property. This is what lets a no-code install (Tag Manager, a WordPress plugin) set the documented variables without writing a rule against any of StepsKit's own classes.
- Attributes —
[data-sk-*]selectors accept any operator. Everything else is limited to a short, frozen list —disabled,hidden,dir,role,type,lang, andaria-*— and only for presence ([role]) or an exact match ([role="button"]); substring operators (^=,*=, …) are refused on all of them. - URLs — a
url(),src(),image(), orimage-set()argument must behttps://…, a root-relative/pathon your own origin, a#fragment, or adata:image/…URI. Plainhttp://, protocol-relative//host, anddata:text/htmlare all refused. - At-rules —
@media,@supports,@container, and@keyframesare allowed.@font-faceis not: the fonts your own stylesheet already loads apply inside every StepsKit surface, shadow-rendered banners and surveys included, so point--sk-font-familyat a family you've already loaded instead of trying to declare one here. - Size — 20 KB (20,480 bytes, measured in UTF-8), enforced on save.
- Errors — a rejected stylesheet is never rewritten. You get back a line and column for every problem, and what's stored is always byte-for-byte what you typed.
Where Custom CSS sits in the cascade
From weakest to strongest, for one property on one element:
- The built-in stylesheet — a
--sk-*variable, or its fallback. - A plain rule in your Custom CSS, no
!important— beats the built-in stylesheet outright. This is the rung for properties the theme never paints: anything without a theme key, plus the ten nullable keys (popoverPadding,popoverRadius,popoverBorderWidth,popoverBorderColor,popoverShadow,popoverMaxWidth,fontFamily,buttonPaddingX,buttonPaddingY,closeButtonColor) while they are unset. - A dashboard theme value — emitted with
!important, so it beats a plain Custom CSS rule. Every other theme key — the colors (bgColor,textColor,buttonColor,buttonTextColor,prevButtonColor,prevButtonTextColor,progressDotColor), font sizes, alignments andbuttonRadius— is painted this way whether or not you ever set it, with its default when you didn't. So a plain.sk-popover { background: … }never wins on a tour popover; for those keys go straight to rung 4. Only the ten nullable keys above wait for you to set them. - Your Custom CSS with
!important, at matching specificity — wins the tie, because Custom CSS is always the last stylesheet applied. Unlike your own page's stylesheet (see How the cascade works above), you don't need to go a specificity level higher here — matching the theme rule's own selector depth is enough. - A per-step override set in the tour builder — inline styles beat
every stylesheet,
!importantor not.
Every dashboard theme key is emitted on either the popover's root class or one of its descendants. Match that same depth in your Custom CSS:
| Theme keys | Custom CSS selector |
|---|---|
bgColor, textColor, popoverPadding, popoverRadius, popoverBorderWidth, popoverBorderColor, popoverShadow, fontFamily | .sk-popover { … } |
popoverMaxWidth | .sk-popover { max-width: …; min-width: …; } — set both, since the theme emits a matching min-width alongside max-width; a lone max-width override still leaves the 250px floor in place. Simpler: set --sk-popover-max-width instead, which drives both by itself. |
titleFontSize, titleAlign, and the title's text color | .sk-popover .sk-popover-title { … } |
bodyFontSize, bodyAlign, and the description's text color | .sk-popover .sk-popover-description { … } |
buttonColor, buttonTextColor, prevButtonColor, prevButtonTextColor, buttonRadius, buttonFontSize, buttonPaddingX, buttonPaddingY | .sk-popover .sk-popover-next-btn { … } and/or .sk-popover .sk-popover-prev-btn { … } |
closeButtonColor (tour popovers) | .sk-popover .sk-popover-close-btn { … } |
buttonsAlign | .sk-popover .sk-popover-footer { … } and .sk-popover .sk-popover-navigation-btns { … } |
progressDotColor | .sk-popover .sk-popover-progress-dot { … } |
| Tooltip theme keys (same shape, painted on the tooltip popover) | .sk-tooltip-popover { … } for root-level keys, .sk-tooltip-popover .sk-popover-title { … } for title keys, and so on |
The tooltip's close-icon color is a special case: it's always painted
by the theme, even when nothing is set (it falls back to the tooltip's own
text color). To override it, you always need
.sk-tooltip-popover .sk-popover-close-btn { color: … !important } —
there's no "unthemed" state to beat instead.
Banners and surveys have no !important theme layer at all — a plain
.sk-banner { … } or .sk-survey { … } rule already wins against their
built-in styles, no !important needed.
Browser support
Custom CSS relies on constructable stylesheets, so it needs a reasonably
current browser: Chrome 73+, Edge 79+, Firefox 101+, or Safari 16.4+. On an
older browser, Custom CSS is skipped entirely — that visitor still gets your
theme and the --sk-* variables, neither of which has a version floor, just
not your Custom CSS rules.
Banners and surveys render in shadow roots
Announcement banners and survey cards mount inside a closed shadow root,
so a class-based rule in your own page's stylesheet can't reach .sk-banner
or .sk-survey at all. CSS custom properties are the exception — they
inherit through the shadow boundary regardless of how the root resets
everything else — which is exactly why --sk-font-family, --sk-banner-*,
and --sk-survey-* work on these surfaces from your stylesheet and class
selectors don't.
Custom CSS is different. It's adopted directly into every banner and
survey shadow root, not read from your page's stylesheet, so a .sk-banner
or .sk-survey rule written in Theme → Custom CSS does reach them — the one
selector-based tier (unlike the classes above) that works identically across
all four surfaces. Host-page rules still can't get in; only what you write
in the Custom CSS panel does.
Dashboard preview
The dashboard's own preview can't see variables set on your site — it renders in the dashboard's origin, not yours. Your actual visitors, on your domain, do see them. If a preview and a live tour ever look different, this is usually why.
Content Security Policy
None of the styling tiers on this page add a CSP requirement of their own —
Custom CSS included. It's delivered through the CSSOM adoptedStyleSheets
API rather than a <style> tag, so it sits outside style-src entirely; a
stricter policy that omits 'unsafe-inline' from style-src doesn't block
it. If you haven't set up your policy yet, see Content Security
Policy for what StepsKit needs
overall — the theme engine's own injected <style> tags are what actually
need style-src 'unsafe-inline', and that requirement is unchanged.