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:

VariableApplies toDefaultTheme settingNotes
--sk-popover-paddingTour & tooltip popovers20pxShape & spacing → PaddingAccepts any padding value, including the two- and four-value forms.
--sk-popover-radiusTour & tooltip popovers12pxShape & spacing → Corner radius
--sk-popover-border-widthTour & tooltip popovers0 (no border)Shape & spacing → Border width
--sk-popover-border-colorTour & tooltip popoverscurrentColorShape & spacing → Border colorOnly visible once the border width is set.
--sk-popover-shadowTour & tooltip popovers0 1px 10px #0006Shape & spacing → ShadowTakes a full box-shadow value; use none to remove it.
--sk-popover-max-widthTour & tooltip popovers300pxShape & spacing → Max widthThe 250px minimum width shrinks with it — see the browser note below.
--sk-font-familyEvery surfacethe built-in Inter stackTypography → FontBanners 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-xTour popovers only16pxButtons → Horizontal paddingHorizontal 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-yTour popovers only8pxButtons → Vertical paddingVertical 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-colorTour popovers only#6b7280Popover → Close iconAlso 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-paddingAnnouncement banner10px 16pxOverrides both the desktop default and the narrow-screen default (10px 12px). No dashboard control yet.
--sk-banner-font-familyAnnouncement bannerthe system-font stackWins over --sk-font-family when both are set. No dashboard control yet.
--sk-survey-paddingSurvey card16px 18pxNo dashboard control yet.
--sk-survey-font-familySurvey cardthe system-font stackWins over --sk-font-family when both are set. No dashboard control yet.
--sk-survey-radiusSurvey card12pxNo 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 px units 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. inherit is a CSS-wide keyword that applies to the custom property itself, not to whatever later consumes it through var() — 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:

AttributeWhereValue
data-sk-tourTour popoverThe tour's slug.
data-sk-tour-idTour popoverThe tour's id — stable across renames (a slug is re-derived from the tour's name on every rename).
data-sk-stepTour popoverThe 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-typeTour popoverThe step type (callout or modal).
data-sk-tooltipTooltip indicator and tooltip popoverThe tooltip's id.
data-sk-announcementAnnouncement bannerThe announcement's id.
data-sk-surveySurvey cardThe 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:

ClassWhereMeaning
sk-activeYour <body>A tour is currently running.
sk-fadeYour <body>Set alongside sk-active when step transitions are animated (the default).
sk-simpleYour <body>Set alongside sk-active instead of sk-fade when animations are turned off.
sk-active-elementThe element the current step is highlightingThis is YOUR element — the one the tour is pointing at, not something StepsKit rendered.
sk-no-interactionThe highlighted elementSet 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) and div: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, or body rule — 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, and aria-* — and only for presence ([role]) or an exact match ([role="button"]); substring operators (^=, *=, …) are refused on all of them.
  • URLs — a url(), src(), image(), or image-set() argument must be https://…, a root-relative /path on your own origin, a #fragment, or a data:image/… URI. Plain http://, protocol-relative //host, and data:text/html are all refused.
  • At-rules@media, @supports, @container, and @keyframes are allowed. @font-face is not: the fonts your own stylesheet already loads apply inside every StepsKit surface, shadow-rendered banners and surveys included, so point --sk-font-family at 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:

  1. The built-in stylesheet — a --sk-* variable, or its fallback.
  2. 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.
  3. 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 and buttonRadius — 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.
  4. 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.
  5. A per-step override set in the tour builder — inline styles beat every stylesheet, !important or 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 keysCustom 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.