Jump to

Styling

The package ships with minimal default styles. To use them:

import 'react-feedback-surveys/index.css';

The defaults are only a starting point. Your CSS changes colors, radius, shadows, font, sizes, spacing, and the look of every part and state, so one widget can look like a chat bubble, a sticky note, a blueprint, or a retro OS window. The markup, the step flow, and the icons stay the same.

Pick the level you need:

  1. Themes: light, dark, or auto. One prop, no CSS.
  2. Common Recipes: brand color, font, corners, and shadows in a few lines of CSS.
  3. Catalog Styles: a ready-made look from the style catalog, copied and changed to fit your brand.
  4. Custom Styles: your own CSS for every part and state.

If a rule of yours doesn't apply, see How Styles Combine.

Themes (Dark Mode)

The theme prop switches the built-in color palette: light, a ready dark theme, or auto. No CSS is needed on your side.

Comparison table
ValueResult
'light'Default palette. Same as leaving the prop out.
'dark'Ready dark palette.
'auto'Follows the visitor's system color scheme and switches when it changes.
<Popup theme="auto">
  <Survey /* ... */ />
</Popup>
  • Set it on the outermost component: Popup or Surface. Components inside inherit the theme. A theme on Survey alone doesn't recolor the Surface around it.
  • The nearest theme wins. theme="light" inside a theme="dark" component restores the light palette.
  • auto is pure CSS (prefers-color-scheme), so there's no flash of the wrong theme with server rendering.
  • Your --ft-* variables always win over the built-in theme, wherever you set them.

To toggle the theme from your own switcher, pass its state to the prop: <Popup theme={isDark ? 'dark' : 'light'}>.

Common Recipes

Set --ft-* variables on :root to change every survey on the page, or on a class of your own to change one (see Custom Styles). The full list is in CSS Variables.

Brand color. The submit button and a checked checkbox use the accent color. Unset, it follows the text color:

:root {
  --ft-color-accent: #6d4aff;
  --ft-color-accent-text: #fff;
}

Your site's font. The widget doesn't take the page font on its own. Give it the same font stack as your site; the font files come from your site:

:root {
  --ft-font-family: Inter, system-ui, sans-serif;
}

Corners. --ft-surface-radius rounds the card, the fields, and the submit button:

:root {
  --ft-surface-radius: 0;
}

No shadow. For a survey placed on a card of your own, or a flat popup:

:root {
  --ft-surface-shadow: none;
  --ft-popup-shadow: none;
}

Popup behind your header or chat widget. Raise its z-index, 49 by default:

:root {
  --ft-popup-z-index: 1000;
}

Tailwind classes. classNames takes utility classes for single parts. With Tailwind CSS v4, import the library styles into the components layer from your CSS entry, not from JavaScript. Then utilities win over them (see How Styles Combine for why):

@import "tailwindcss";
@import "react-feedback-surveys/index.css" layer(components);
<Survey
  classNames={{
    submit: 'rounded-full bg-indigo-600 text-white hover:bg-indigo-500',
    field: 'rounded-lg border-gray-300'
  }}
  /* ... */
/>

CSS Modules and CSS-in-JS. Anything that gives you a class name works the same way:

import styles from './Feedback.module.css';

<Survey
  classNames={{ submit: styles.submit }}
  /* ... */
/>

Catalog Styles

Style catalog

The style catalog in Storybook has ready-made styles, from minimal to a hand-drawn sketch, each paired with a use case.

Each style is a class, such as terminal. Its CSS also expects two wrapper classes:

  • ftools-survey on every survey;
  • ftools-embed for an inline survey, or ftools-popup for a popup.

The Feedback Tools SDK puts the same classes on the host of its widget. So one stylesheet works both in your app and in the SDK widget.

To use a style:

  1. Open a style in the catalog. Its page shows it on every survey type.

  2. Click Copy above its CSS and paste the CSS into your stylesheet, outside any @layer.

  3. Wrap the widget in an element with the style's classes, and pass the theme named in the comment at the top of the CSS:

    <div className="ftools-survey ftools-embed terminal">
      <Surface theme="dark">
        <Survey /* ... */ />
      </Surface>
    </div>

    For a popup, wrap Popup and use ftools-popup instead of ftools-embed:

    <div className="ftools-survey ftools-popup terminal">
      <Popup theme="dark">
        <Survey /* ... */ />
      </Popup>
    </div>
  4. Change the CSS to fit your brand, starting with the --ft-* variables at the top.

Catalog styles are examples to copy, not part of the package: they aren't versioned or updated with the library, and the CSS you paste is yours to change.

The same CSS works with and without a shadow root. Every rule has two selectors: ::part() for a widget inside a shadow root, and [part~="..."] for a widget in the page's own DOM:

.ftools-survey.terminal::part(choice checked),
.ftools-survey.terminal [part~="choice"][part~="checked"] {
  /* ... */
}

Write your own stylesheet the same way when it has to work in both places.

Custom Styles

Common Recipes set variables on :root and change every survey on the page. To style one survey only, put the variables on a class of your own and add classes for single parts. A custom style works on top of any built-in theme:

.brand-style {
  --ft-color-text: hsl(250 60% 20%);
  --ft-color-bg: hsl(250 100% 98%);
  --ft-surface-radius: 0;
}

.brand-submit {
  text-transform: uppercase;
}
<Surface className="brand-style">
  <Survey
    classNames={{ submit: 'brand-submit' }}
    /* ... */
  />
</Surface>

For a ready-made start, see Catalog Styles.

The reference sections below list everything you can change: CSS Variables, Custom Classes, and Shadow DOM Parts. Storybook's Styling Cases section shows more combinations.

How Styles Combine

Three rules define every override:

  1. Library styles are defaults. All of them live in one CSS cascade layer, @layer react-feedback-surveys. Any CSS of yours outside a layer wins over them, whatever its specificity and load order. A plain .my-button beats the library's .button:hover:not(:disabled).
  2. Values flow through variables. Colors, radius, shadows, and font come from CSS variables. From weakest to strongest: library defaults → the theme prop → your --ft-* variables. --ft-* set on any ancestor reach the widget, also across a shadow DOM boundary.
  3. Rules come from classes. className and classNames attach your classes to specific parts; inside a shadow root ::part() does the same. A rule that sets a color itself owns that color in every theme. The built-in theme does not adjust it.

If your own CSS uses cascade layers, layer order decides instead of rule 1:

  • Tailwind CSS v4: import the styles into the components layer from your CSS entry instead of JavaScript, so they beat Preflight and utilities beat them:

    @import "tailwindcss";
    @import "react-feedback-surveys/index.css" layer(components);
  • A global reset outside layers (normalize, Bootstrap Reboot, * { margin: 0 }) also wins over the defaults, the same as your classes do. Move it into a layer declared before the library's. Layer order is fixed by first appearance, so import the library styles from this same file, not from JavaScript:

    @layer reset, react-feedback-surveys;
    @import "bootstrap/dist/css/bootstrap-reboot.css" layer(reset);
    @import "react-feedback-surveys/index.css";

Inside a shadow root page styles don't reach the widget, so only the CSS you put in that root takes part. To style it from the page, use Shadow DOM Parts.

What's Stable

The styling API follows semver. These names don't change or disappear outside a major release, and every change to them is listed in the changelog:

  • the --ft-* variables from CSS Variables;
  • the classNames keys and part names from Custom Classes, and the state names from Shadow DOM Parts;
  • the theme, className, and classNames props;
  • the cascade layer name, react-feedback-surveys.

Everything else is internal: the library's own class names (hashed, like _submit_1f9cv_274), the DOM structure between parts, and the default values. A minor release can change the default look; your variables and rules keep working on top of it. Catalog styles are examples, not part of this API.

CSS Variables

Set these variables on :root, on a wrapper class, or on a shadow host. Color variables take any CSS color: #6d4aff, rgb(), hsl(), oklch(), a color name, or transparent. Color defaults below are for the light theme. theme="dark" changes all of them except --ft-color-star.

Colors

Comparison table
VariableDefaultWhat it changes
--ft-color-texthsl(30 8% 14%)Headings and body text.
--ft-color-bghsl(0 0% 100%)Survey background.
--ft-color-mutedhsl(222 11% 46%)Labels and secondary text.
--ft-color-controlhsl(214 20% 96%)Background of inputs and buttons.
--ft-color-borderhsl(214 14% 83%)Borders of inputs and containers.
--ft-color-outlinehsl(218 14% 65%)Focus outline.
--ft-color-accent--ft-color-textSubmit button and a checked checkbox.
--ft-color-accent-text--ft-color-bgText and check mark on top of the accent.
--ft-color-starhsl(42 99% 64.5%)Fill of a highlighted star in the stars scale.
--ft-color-errorhsl(32 95% 44%)Validation borders and outlines.
--ft-color-error-texthsl(32 95% 32%)Error text of a failed screenshot capture. Darker than --ft-color-error for text contrast.
--ft-color-shadowhsl(0 0% 0%)Color of the default surface and popup shadows.

Surface and popup

Comparison table
VariableDefaultWhat it changes
--ft-surface-radius8pxCorners of the card, inputs, and submit button.
--ft-surface-padding20pxCard padding.
--ft-surface-padding-mobile20pxCard padding on screens narrower than 400px.
--ft-surface-shadowsee belowShadow of an inline survey card. none removes it.
--ft-popup-shadowsee belowShadow of the popup card. none removes it.
--ft-popup-z-index49Z-index of the popup.

Other

Comparison table
VariableDefaultWhat it changes
--ft-font-family'Helvetica Neue', 'Arial Nova', Helvetica, Arial, sans-serifFont of all text, inputs, and buttons.
--ft-checkbox-size20pxSize of a choice checkbox. The box stays centered on the first text line at any size.

A custom shadow value replaces all default layers. The defaults are:

:root {
  --ft-surface-shadow:
    0 0 1px hsl(0 0% 0% / 12%),
    0 1px 2px hsl(0 0% 0% / 6%),
    0 2px 4px hsl(0 0% 0% / 4%);

  /* the surface shadow plus a wider layer that lifts the popup above the page */
  --ft-popup-shadow:
    0 0 1px hsl(0 0% 0% / 12%),
    0 1px 2px hsl(0 0% 0% / 6%),
    0 2px 4px hsl(0 0% 0% / 4%),
    0 16px 40px -12px hsl(0 0% 0% / 14%);
}

Custom Classes

Survey accepts a flat classNames object: one key per part. Your classes win over the library defaults (see How Styles Combine). Every part also carries a part attribute with the same name in kebab case, for styling it inside a shadow root (see Shadow DOM Parts).

Names follow four rules: buttons are named by their action (submit, skip, attach, remove, dismiss), lists are plural and their items singular (choices → choice, points → point), a part inside another part takes its name as a prefix (point-icon, success-icon), and a state is the part's name plus an adjective (checkboxChecked, fieldInvalid), so each state class lands on exactly one part.

Comparison table
KeyPartApplies to
rootrootSurvey wrapper
headheadRow with the title
titletitleQuestion, step question or thank-you text
bodybodyContent under the head
successIconsuccess-iconIcon on the success step
scalescaleRating scale: the points and the legend
pointspointsRow of rating points
pointpointSingle rating point (button)
pointIconpoint-iconEmoji, star or thumb inside a point
pointScorepoint-scoreNumber inside a point
legendlegendMin and max captions under the scale
formformForm on the feedback and email steps
subtextsubtextText above the email field
choiceschoicesList of predefined choices
choicechoiceSingle choice (the clickable label)
checkboxcheckboxVisible checkbox square of a choice
fieldfieldEvery text field: feedback textarea, "Other" input, email input
attachattachScreenshot capture button
attachmentattachmentAttached screenshot row
removeremoveButton that removes an attachment
errorerrorCapture error row
dismissdismissButton that hides the capture error
actionsactionsWrapper for the submit and skip buttons
submitsubmitSubmit button
skipskipSkip button

The layout components have their own: Popup's className styles the popup (part popup), Popup's classNames.surface and Surface's className style the card (part surface), and Popup's classNames.close styles the close button (part close).

Form keys apply to both the feedback and the email step. To style one step only, scope the rule with a step key.

State and step keys add a class next to the part's own while it is in that state, so combine the two:

Comparison table
KeyAdded toWhile
choiceCheckedchoiceThe choice is checked
checkboxCheckedcheckboxIts choice is checked
fieldInvalidfieldThe field shows an error
attachBusyattachA screenshot is being captured
pointIconFilledpointIconThe star is filled (stars scale only)
rating, feedback, contact, successrootThat step is shown

A disabled button needs no key: use .my-submit:disabled.

import { Survey } from 'react-feedback-surveys';
import 'react-feedback-surveys/index.css';

<Survey
  classNames={{
    point: 'my-point',
    choice: 'my-choice',
    checkbox: 'my-checkbox',
    checkboxChecked: 'my-checkbox-checked',
    submit: 'my-submit'
  }}
  type="csat"
  points={5}
  scaleStyle="numbers"
  question="How would you rate your satisfaction with our product?"
  minLabel="Very unsatisfied"
  maxLabel="Very satisfied"
  thankYouMessage="Thanks for your feedback!"
  onScoreSubmit={({ value }) => {/* ... */}}
  onFeedbackSubmit={({ value, text }) => {/* ... */}}
/>
.my-point { border-radius: 10px; }
.my-checkbox-checked { background-color: #6d4aff; }
.my-submit:disabled { opacity: 0.4; }

Shadow DOM Parts

When the widget renders inside a shadow root (as the Feedback Tools SDK does), page CSS can't reach its classes. Every part carries a part attribute instead, so page CSS styles it through the shadow host with ::part():

.my-host {
  --ft-color-bg: #f7f5ff;
  --ft-surface-radius: 18px;
}

.my-host::part(submit) {
  color: #fff;
  background-color: #6d4aff;
  border-radius: 999px;
}

.my-host::part(choice checked) {
  background-color: #ede9fe;
}

Part names are the classNames keys in kebab case, plus popup, surface, and close from the layout components. The full list is in Custom Classes.

States come as a second part name, because ::part() can't look at attributes or neighbours:

Comparison table
StateSelector
Checked choice::part(choice checked), ::part(checkbox checked)
Invalid text field::part(field invalid)
Screenshot capture in progress::part(attach busy)
Filled star::part(point-icon filled)
Current step::part(root rating), feedback, contact, success

Pseudo-classes and pseudo-elements of the part itself work: :hover, :focus-visible, :disabled, ::before, ::after, ::placeholder. Selectors that look inside or around a part don't: no :has(), no ::part(a) .child, no ::part(a) + ::part(b).

Outside a shadow root ::part() matches nothing, but the part attributes are still there: .my-host [part~="choice"][part~="checked"] styles the same part in the page's own DOM.