This is a reference for customers who want Outseta's sign-up, login, and profile embeds to match their app's look and feel. It focuses on the strategy and the selectors. The values you apply (colors, fonts, radii, spacing) are up to your design system, so the examples below leave property values empty for you to fill in with whatever your app uses.
Where to put the CSS
You've got three options, depending on how your site is built:
-
In Outseta. Go to Auth → Embeds and click Customize design. At the bottom of the list of design customization options you'll see Custom CSS. Pop it in there, click Save, and we'll load the CSS for you when we serve the embeds to your site.
-
In your site builder's custom code area. Builders like Webflow, Framer, and Squarespace each have a place to paste custom code. Just remember: if it's a slot designated for
<head>code, you'll need to wrap your CSS in<style>tags. -
In your app's stylesheet. If you're building a custom app (React, Next.js, Rails, plain HTML, etc.), drop the CSS straight into whichever stylesheet is already loaded on the pages that render Outseta embeds.
The rules and selectors below work the same in all three places.
Two strategies, used together
Every customer override builds on the same two techniques. You don't pick one or the other — you use both, and the first does most of the work.
-
Override Outseta's CSS variables at the widget scope. Outseta's default stylesheet reads colors, sizes, and weights from variables like
--color-text,--accent-color,--color-coal,--font-size-sm, and--weight-bold. Re-pointing those at your design tokens re-themes large portions of every embed in a handful of lines — no selector wrestling. -
Scoped selectors prefixed with
.o--Reset--scopefor the parts variables don't cover (layout, focus rings, button backgrounds that aren't on the accent, show/hide rules). Outseta adds.o--Reset--scopeon an ancestor of every widget subtree, and Outseta's own rules don't include it — so any selector you prefix with.o--Reset--scopeis automatically one class more specific than Outseta's internal rules and wins the cascade.
The rest of this article is organized the same way: set the variables first, then the selectors fill in the gaps.
Variable overrides
Set these once at .o--Reset--scope .o--Widget--widget and they cascade through every embed — inline and popup, light and dark, nav tabs, password toggle, muted hints, badges, borders, and focus accents.
.o--Reset--scope .o--Widget--widget {
/* Typography scale */
--font-size-sm: ...;
--font-size-xs: ...;
--weight-bold: ...; /* Outseta uses 700 by default; most design systems want 500 */
/* Text colors — set both the base and the -dark variant even if your app
is light-only, because Outseta picks the -dark variable when the widget's
displayMode is dark or auto. */
--color-text: ...; /* default body text */
--color-text-dark: ...;
--color-text-secondary: ...; /* muted/hint text, password toggle, etc. */
--color-text-secondary-dark: ...;
/* Input / select borders */
--color-coal: ...;
--color-coal-dark: ...;
/* Structural borders (nav divider, separators) */
--color-slate: ...;
--color-slate-dark: ...;
/* Accent (primary button, focus, active tab) */
--accent-color: ...;
/* Background */
--color-bg: ...;
--dark-mode-bg-color: ...;
}
Why set both -dark and non--dark pairs? Outseta switches between them based on the widget's displayMode (light vs dark/auto). Set both to the same value from your design tokens and the embed respects whichever mode Outseta is in without you having to care.
After variables do the heavy lifting, everything below is for the parts Outseta doesn't route through a variable (layout, focus rings, button backgrounds, etc.) or where you want finer-grained control than the variable offers.
The .o--Reset--scope pattern
Every selector in the reference below assumes you prefix it with .o--Reset--scope. The pattern is:
.o--Reset--scope .o--Widget--widget <target selector> {
/* your overrides */
}
Think of .o--Reset--scope .o--Widget--widget as your baseline scope — all your embed overrides live inside it.
Extra specificity via tag + class. Where you see input.o--Input--input, button.o--Button--btn, or label.o--Label--label below, the tag prefix adds one point of type specificity — useful if Outseta ever uses a more deeply compound selector of their own that would otherwise tie on class count and win on source order.
Dashboard settings and CSS overrides
Once you're overriding with the variable + .o--Reset--scope patterns above, the dashboard's design settings (Outseta → Auth → Embeds → Design) don't affect your app's visual output — your CSS wins regardless of the font, color mode, accent color, button style, field style, border width, and corner shape you've picked. You can leave the dashboard at its defaults and drive all visuals from CSS.
Selector reference
Each section below gives:
- What the selector targets
- The selector itself
- Some properties you may want to set
Widget wrapper (inline embeds)
The outermost element of an inline (non-popup) embed. Outseta paints a light panel here by default; most customers want it transparent so the container you wrap the embed in (a card, a section, etc.) provides the visuals.
.o--Reset--scope .o--Widget--widget:not(.o--Widget--popup) {
/* background-color: transparent */
}
Inline embed header + body
If you render an embed inline inside your own card, with your own heading, Outseta's built-in heading inside the widget becomes redundant. Hide it, and zero out the inner body padding so your own card handles layout.
.o--Reset--scope .o--Widget--widget:not(.o--Widget--popup) .o--header,
.o--Reset--scope .o--Widget--widget:not(.o--Widget--popup) .o--SectionGroup--sectionGroup:has(h1) {
display: none;
}
.o--Reset--scope .o--Widget--widget:not(.o--Widget--popup) .o--Widget--widgetBody {
padding: 0;
}
Widget wrapper (popup embeds)
The outermost element of a popup embed (e.g. the "Set password" confirmation, profile popup). Unlike inline, popups don't have a customer-provided container, so they need their own background, border, and shadow.
.o--Reset--scope .o--Widget--widget.o--Widget--popup {
/* background-color: ...; color: ...; box-shadow or ring: ...; */
}
Labels
Field labels above inputs. The text color is already handled by the --color-text variable; this rule is for weight and any label-specific overrides.
.o--Reset--scope .o--Widget--widget label.o--Label--label {
/* font-weight: ...; color: ... */
}
Inputs and textareas
The text fields inside every embed (email, password, name, etc.). Border color comes from --color-coal / --color-coal-dark — set those at the widget scope and the default border matches your design system. This rule handles shape, padding, size, placeholder color, and focus.
.o--Reset--scope .o--Widget--widget input.o--Input--input,
.o--Reset--scope .o--Widget--widget textarea.o--TextArea--textArea {
/* height, padding, font-size */
/* background-color */
/* border, border-radius */
/* transition */
}
/* Textarea should typically grow */
.o--Reset--scope .o--Widget--widget textarea.o--TextArea--textArea {
/* height: auto; min-height: ...; resize: vertical */
}
/* Focus — override both :focus and :focus-visible, and reset outline */
.o--Reset--scope .o--Widget--widget input.o--Input--input:focus,
.o--Reset--scope .o--Widget--widget input.o--Input--input:focus-visible,
.o--Reset--scope .o--Widget--widget textarea.o--TextArea--textArea:focus,
.o--Reset--scope .o--Widget--widget textarea.o--TextArea--textArea:focus-visible {
/* border-color, box-shadow (focus ring), outline: none */
}
Focus state. Target both :focus and :focus-visible — Outseta's default CSS adds an outline on :focus that will bleed through if you only override :focus-visible. Also reset outline explicitly.
Select dropdowns
Native <select> elements (used for e.g. country / timezone picks in profile forms).
.o--Reset--scope .o--Widget--widget .o--Select--selectWrapper select {
/* height, padding, font-size */
/* background-color */
/* border, border-radius */
}
/* Focus — doubling `.o--Select--selectWrapper` in the selector bumps class
specificity by one to outrank Outseta's :focus / :active rules */
.o--Reset--scope .o--Widget--widget .o--Select--selectWrapper.o--Select--selectWrapper select:focus,
.o--Reset--scope .o--Widget--widget .o--Select--selectWrapper.o--Select--selectWrapper select:focus-visible {
/* border-color, box-shadow (focus ring), outline: none */
}
Buttons
The primary button inside every embed (Sign up, Log in, Set password, Save). Background uses the --accent-color variable by default, so setting that at the widget scope is usually enough — this rule is for shape, size, font, hover, and focus.
.o--Reset--scope .o--Widget--widget button.o--Button--btn {
/* width, height, padding */
/* font-size, font-weight */
/* background-color, color */
/* border, border-radius */
/* transition, hover state */
}
.o--Reset--scope .o--Widget--widget button.o--Button--btn:focus-visible {
/* border-color, box-shadow */
}
Button label
The text inside a button is wrapped in .o--Button--children. Outseta colors it via a separate, high-specificity rule — simply setting color on .o--Button--btn won't cascade down to the label.
.o--Reset--scope .o--Widget--widget .o--Button--btn .o--Button--children,
.o--Reset--scope .o--Widget--widget button.o--Button--btn * {
/* color: ... */
}
The trailing * selector is a catch-all for anything else inside the button (icons, loading spinners, etc.). Keep it.
Cancel button
Popup confirmations (e.g. "Delete account," "Cancel subscription") render a Cancel button alongside the primary action. Outseta gives it the same solid styling as the primary by default, which you probably don't want. Target buttonType-cancel specifically.
.o--Reset--scope .o--Widget--widget button.o--Button--btn.o--Button--buttonType-cancel {
/* background-color, color, hover state */
}
.o--Reset--scope .o--Widget--widget .o--Button--btn.o--Button--buttonType-cancel .o--Button--children {
/* color: ... (secondary-foreground equivalent) */
}
Links
Text links inside the embed, like "Forgot password?" or "Already have an account?"
.o--Reset--scope .o--Widget--widget a,
.o--Reset--scope .o--Widget--widget a:visited,
.o--Reset--scope .o--Widget--widget a * {
/* color, font-weight */
/* text-decoration: none */
/* text-underline-offset */
/* text-underline-position: auto — Outseta sets this to "under" which looks wrong */
}
/* Hover — exclude the popup close button so the "X" doesn't get underlined */
.o--Reset--scope .o--Widget--widget a:not(.o--CloseLink--close):hover,
.o--Reset--scope .o--Widget--widget a:not(.o--CloseLink--close):hover * {
/* text-decoration: underline */
}
Dividers and separators
Section dividers ("Or continue with" style labeled rules, and plain separators).
/* The text inside an "Or continue with" divider */
.o--Reset--scope .o--Widget--widget .o--HrLabel--hrLabel:after {
/* padding, color, background */
}
/* The line itself */
.o--Reset--scope .o--Widget--widget .o--HrLabel--hrLabel:before {
/* background (border color) */
}
/* Plain separators */
.o--Reset--scope .o--Widget--widget .o--Separator--displayMode-light,
.o--Reset--scope .o--Widget--widget .o--Separator--displayMode-dark {
/* border-top-color */
}
Profile widget nav (tabs)
The Profile / Account / Plan tabs on the left side of the profile popup. Most of the heavy lifting is already done for you: the right-hand border between tabs and content comes from --color-slate / --color-slate-dark, and Outseta paints the active tab's background via a li:before pseudo-element tinted by --accent-color. With those variables set, you only need to tune the tab link text color, the active tab text color, the pill's border-radius, and the hover underline.
/* Inactive tab text */
.o--Reset--scope .o--Widget--widget .o--NavDesktop--navDesktop li a {
/* color: ... (muted) */
}
/* Active tab text — Outseta paints the pill background from --accent-color,
so this is just the foreground that reads against it */
.o--Reset--scope .o--Widget--widget .o--NavDesktop--navDesktop li.o--NavDesktop--active a {
/* color: ... */
}
/* Active tab pill — Outseta uses a `li:before` pseudo-element for the fill.
Override its border-radius to match your design system's pill shape. */
.o--Reset--scope .o--Widget--widget .o--NavDesktop--navDesktop ul li:before {
/* border-radius: ... */
}
/* Kill the tab hover underline inherited from the link rules */
.o--Reset--scope .o--Widget--widget .o--NavDesktop--navDesktop li a:hover {
text-decoration: none;
}
The active tab's background is not on the <a> — it's on a ::before pseudo-element of the parent <li>.
Payment term toggle
The "Monthly / Yearly" toggle on billed plans.
.o--Reset--scope .o--Widget--widget .o--HorizontalToggle--horizontalToggle ul {
/* background-color, border, padding, border-radius */
}
.o--Reset--scope .o--Widget--widget .o--HorizontalToggle--horizontalToggle li a {
/* border, border-radius, color (inactive state) */
}
.o--Reset--scope .o--Widget--widget .o--HorizontalToggle--horizontalToggle li.o--HorizontalToggle--active a {
/* border-color, background-color, color (active state) */
}
Badges
Plan tier badges ("Free," "Pro," etc.) in the profile embed.
.o--Reset--scope .o--Widget--widget .o--Badge--badge {
/* font-size, gap, background-color, color */
}
Charge summary (billing)
The totals row on checkout forms.
/* Emphasize the final total row */
.o--Reset--scope .o--Widget--widget .o--ChargeSummary--invoiceRow:last-child .o--ChargeSummary--invoiceRowWhen,
.o--Reset--scope .o--Widget--widget .o--ChargeSummary--invoiceRow:last-child .o--ChargeSummary--invoiceRowPrice {
/* font-weight */
}
/* Hide the informational footer text */
.o--Reset--scope .o--Widget--widget .o--ChargeSummary--info {
display: none;
}