HomeDocsUsing ReqioCustom CSS
Using Reqio
Custom CSS
Write safe custom CSS for the Reqio widget: which selectors are stable, which CSS constructs are blocked and why, the size cap, and worked examples.
Custom CSS is a raw styling escape hatch for the widget, on top of the five built-in Theme controls (Color mode, Corner radius, Elevation, Density, Font). Use it for anything those controls do not cover. This page is the full reference: what your CSS can and cannot do, exactly which selectors are safe to target, and why the rest are blocked.
Before you start
- A Reqio project on the Pro plan or above
- Working knowledge of CSS custom properties and attribute selectors
- The field itself, at Project → Settings → Widget → Theme → Custom CSS
This page is written for an AI agent too
If you were handed this URL by Reqio's dashboard to write CSS on someone's behalf, every fact below is verified against the widget's source, not guessed. Read "Safe selectors and hooks" and "Design tokens" before writing a single rule, and check "Troubleshooting" before assuming a rule failed silently for no reason.
Quick facts
- Field
- Project → Settings → Widget → Theme → Custom CSS
- Plan required
- Pro or above. Same
customBrandinggate as the custom logo and the Powered-by-Reqio removal toggle. Free always renders the default theme. - Size cap
- 10,240 bytes (10 KB), measured as UTF-8 byte length. For plain-ASCII CSS this matches the character count shown next to the field; non-ASCII characters count as more than one byte each.
- Cascade position
- Applied last, inside the widget's own Shadow DOM, after the base token stylesheet and after anything the Theme controls override.
- Injection method
- Parsed as a stylesheet (
CSSStyleSheet.replaceSync+adoptedStyleSheets), neverinnerHTML. It cannot introduce an element, an event handler, or a script. - Scope
- The widget's Shadow DOM only. Nothing in your CSS reaches the rest of your page, and nothing on your page overrides the widget.
Add or edit your CSS
Open the field
Go to Project → Settings → Widget, scroll to the Theme section, and find Custom CSS near the bottom, below the five segmented controls.
Write your CSS
Use the selectors and tokens documented below. The field has no live preview for Custom CSS specifically, the widget preview panel reflects it once saved.
Save and verify
Save the project. Reload a page with the widget embedded and open it. If a rule appears to do nothing, check Troubleshooting before assuming it was silently dropped.
Where your CSS runs
Every widget instance mounts inside a closed-off Shadow DOM. Three layers stack up inside it, in this order:
- Base tokens and component styles. Written once at mount, as the widget's own constructed stylesheet.
- Theme control overrides. Any of the five segmented controls that is not
Default(or the Primary color field) writes its own CSS variable directly onto the widget's root element as an inline style. Inline styles beat every stylesheet rule for that same property, regardless of load order, see Troubleshooting. - Your Custom CSS. Appended after the base stylesheet as its own constructed stylesheet, so it wins ties against layer 1 by normal cascade order. It does not automatically win against layer 2.
A closed boundary, both directions
Shadow DOM isolation runs both ways. Your Custom CSS can never leak out and restyle anything on your page, and nothing on your page (resets, utility classes, !important rules elsewhere) reaches in and restyles the widget. The one exception is ordinary CSS inheritance of properties like cursor and font-family, which the widget already pins to its own values so your page's styles do not bleed through unless you want them to (see the Font control's "Match my site" option).
What gets rejected, and why
Every save runs through a server-side sanitizer before your CSS is stored, decoding CSS backslash escapes first so an obfuscated form of a blocked construct (for example \75rl( for url() is caught the same as the literal text. A rejected save is not silently truncated, it fails outright and the previous saved value stays in effect.
@importrejectedurl(...)rejectedbackground-image or similar can silently call out to an attacker-controlled host on every render. Also the way a remote font would be loaded, which is why remote fonts are not supported.image-set(...)rejectedurl(), for responsive image sources.expression(...)rejected-moz-bindingrejectedEverything else is permitted, including arbitrary layout, color, typography, animation, and pseudo-elements. The reject list targets exfiltration and code-execution vectors specifically, not creative freedom inside the widget's own shadow root, which is the accepted and intended use of this field.
Safe selectors and hooks
This is the part to get right. The widget's own component styles are CSS Modules, and the class names they compile to (things like _5y) are machine-generated on every widget.js build. The generator walks every component stylesheet in the whole widget source tree, in sorted order, and assigns each class name a short counted token. Adding, removing, or renaming any component file anywhere in the widget, not just the one you are trying to style, shifts every name that comes after it alphabetically. These names carry no compatibility guarantee and are reassigned on every deploy, whether or not that deploy touched anything visual.
Never target a generated class name
Anything shaped like ._5y, ._x4f9a2, or (in a local dev build) ._badge_wxejy_11 is off limits. A rule written against one of these will very likely stop matching after the next Reqio deploy, with no warning and no error, it just quietly stops applying.
The selectors below are stable because they come from the widget's own JavaScript (attributes and classes it sets deliberately) or from HTML semantics (tag names, ARIA roles) rather than from the CSS Modules build step. Use these.
| Selector | Targets | Why it is stable |
|---|---|---|
:host | The widget's shadow root element itself. The top-level scope for any global override. | Set by the widget's mount code, not the CSS build. |
:host(.dark) | The widget while in dark mode. | Same. Higher CSS specificity than plain :host, see the note below. |
:host([data-position="BOTTOM_RIGHT"]) (also BOTTOM_LEFT, TOP_RIGHT, TOP_LEFT) | Position-conditional rules for the closed launcher (bubble or tab). | The widget writes this exact attribute and value from the Launcher position setting. |
:host([data-color-mode-pin="LIGHT"]) / ="DARK" | Present only once the Color mode control is pinned away from Auto. | Written by the same code that toggles .dark. |
:host [role="dialog"] | The entire open panel (every flow, every screen), and nothing else. Excludes the closed launcher bubble/tab, which lives outside this element. | role="dialog" is a fixed accessibility attribute on the panel's root element. |
:host [data-viewport="mobile"] / ="desktop" | The open panel, by responsive breakpoint. | Same root element, set from a live media query. |
:host [data-placement="CENTER"] / ="CORNER" | The open panel, by the project's Panel placement setting. | Same root element. |
Design tokens (--color-*, --shadow-*, --radius-*, --space-*, --font-*) declared on :host | Whatever component reads that variable, wherever it appears in the widget. | See the table below. Some are also written inline by the Theme controls, see Troubleshooting. |
:host button, :host textarea, :host input, :host a | Every element of that tag, anywhere in the widget. | Tag names are HTML, not build output. Blunt: this restyles every button in the widget, not one specific button. |
There is currently no way to target a single specific button, card, or row inside the widget by a stable selector. The widget does not expose ::part() / exportparts or any data-reqio-* styling hook on individual components today, only the container-level and role-level hooks above. If you need to style one exact element and not its siblings, you are limited to the design tokens that element happens to read (which may also be read by other elements) or the blunt tag-name selectors. This is a real, current gap, not something you are missing.
Design tokens available on :host
These are the CSS custom properties the widget's own components read for color, spacing, shape, and type. Overriding one on :host changes it everywhere that token is used. The complete live list can always be read directly from the browser: open devtools, find #reqio-widget-root in the Elements panel, expand its #shadow-root (open) node, and inspect the :host rule in the Styles panel.
Color
| Variable | Used for |
|---|---|
--color-bg-surface | The panel's outer background. |
--color-bg-raised | Cards, rows, and raised surfaces inside the panel. |
--color-bg-sunken | Recessed wells (inputs, the activity feed background). |
--color-text-primary | Primary text color. |
--color-text-secondary | Secondary text (labels, meta text). |
--color-text-muted | Muted/helper text. |
--color-border-subtle | Hairline dividers and card edges. |
--color-border-focus | Focus ring color. |
--color-brand-solid | The saturated brand fill (primary buttons, active states). |
--color-status-needs-action-bg, --color-status-in-progress-bg, --color-status-completed-bg | Status pill backgrounds on the board, one variable per pipeline stage. |
--color-chat-bubble-team-bg, --color-chat-bubble-requester-bg | Chat bubble backgrounds, team side and visitor side. |
--color-category-feature-bg, --color-category-error-bg, --color-category-feedback-bg, --color-category-other-bg | Category chip backgrounds. |
--color-brand-primary cannot be overridden on :host
The Primary color field always writes --color-brand-primary as an inline style on the widget's host element, on every load, whether or not you touch Custom CSS. Inline styles win over any stylesheet rule for that same element, so a :host { --color-brand-primary: ... } rule in your Custom CSS is silently ignored. Use the Primary color field to change it everywhere, or see the scoped-override example below to change it in just the open panel.
Shape, shadow, and spacing
| Variable | Used for |
|---|---|
--radius-sm, --radius-base, --radius-full | Corner rounding, from small chips up to fully-rounded pills. |
--shadow-raised, --shadow-sunken | The widget's neumorphic depth effect on raised and recessed surfaces. |
--shadow-bubble, --shadow-floating | The closed launcher and floating panel shadows. |
--space-1 through --space-8 | The spacing scale used for padding and gaps throughout. |
Some of these are also written by the Theme controls
--radius-sm / --radius-base (Corner radius), --shadow-raised / --shadow-sunken (Elevation), and --space-3 through --space-6 (Density) are the exact variables those Theme controls write inline when set to anything other than Default. If your Custom CSS override of one of these has no effect, check whether the matching control is non-default first, see Troubleshooting.
Typography
| Variable | Used for |
|---|---|
--font-family-sans | The widget's font stack. Also written inline when the Font control is System or Match my site. |
--font-weight-regular, --font-weight-semibold, --font-weight-bold | Font weights. |
--font-size-xs, --font-size-sm, --font-size-base, --font-size-md, --font-size-lg | The type scale. --font-size-base and --font-size-md are also written inline by the Density control. |
--line-height-tight, --line-height-snug, --line-height-normal | Line heights. |
Worked examples
Recolor a surface, in both modes
A :host(.dark) rule has higher CSS specificity than a plain :host rule (the pseudo-class argument counts like a class), so it always wins for the same property regardless of which stylesheet loads later. Writing only a bare :host { } block for a token that differs between light and dark will apply in light mode but get overridden again by the base stylesheet in dark mode. Write both blocks:
:host {
--color-bg-raised: #f4f1ea;
--color-border-subtle: #ded7c6;
}
:host(.dark) {
--color-bg-raised: #1c1a16;
--color-border-subtle: #322f28;
}Soften corners and shadows without the Theme control
This only works if the Corner radius and Elevation Theme controls are left on Default. If either is set to something else, that control's inline override wins over this rule, use the control instead for that property.
:host {
--radius-base: 1.25rem;
--radius-sm: 0.75rem;
--shadow-raised: 0 1px 2px rgba(0, 0, 0, 0.08);
}A second brand color, scoped to just the open panel
--color-brand-primary cannot be overridden at :host (the Primary color field always sets it inline there), but a descendant selector is not fighting that inline style, it is declaring a new value for its own subtree. :host [role="dialog"] matches only the open panel, never the closed launcher bubble, which lives outside it:
:host [role="dialog"] {
--color-brand-primary: #7c3aed;
}Native-element fallback
When no container-level hook fits, plain tag selectors still work. They are blunt: this changes every textarea and text input in every flow, not just one.
:host textarea,
:host input[type="text"] {
font-size: 1rem;
}Troubleshooting: my rule did nothing
| Symptom | Likely cause | Fix |
|---|---|---|
Selector looks like ._5y or ._badge_wxejy_11 | These are regenerated on every widget.js build and are not a stable contract, see Safe selectors above. | Target :host, a design token, a role/data attribute, or a plain tag selector instead. |
Rule sets --color-brand-primary on :host | The Primary color field always writes this inline, which beats any stylesheet rule. | Use the Primary color field, or scope your override to a descendant like :host [role="dialog"]. |
Rule sets --radius-sm, --radius-base, --shadow-raised, --shadow-sunken, --space-3 through --space-6, --font-size-base, --font-size-md, or --font-family-sans on :host | The matching Theme control (Corner radius, Elevation, Density, or Font) is not set to Default, and writes an inline override for the same variable. | Set that one control back to Default, or use the control instead of Custom CSS for that property. |
A :host { } override works in one color mode but not the other | :host(.dark) has higher specificity than plain :host, so the base stylesheet's dark-mode rule wins regardless of load order. | Add a matching :host(.dark) { } block with the dark-mode value. |
| Nothing changed after saving | The field showed a size or validation error and the save was rejected. The previous saved value (or nothing) is still live. | Remove @import, url(), image-set(), expression(), or -moz-binding (including an escaped or commented form), or shorten under 10,240 bytes. |
| Every rule seems to be ignored | The project is on the Free plan. | Custom CSS requires Pro or above. |
| Selector never matches anything | It was copied from inspecting the host page's normal DOM, not the widget's Shadow DOM. | In devtools, find #reqio-widget-root, expand its #shadow-root (open) node, and inspect inside it. |