| name | customizing-clerk-components |
|---|---|
| description | Customize the look of Clerk's prebuilt components (SignIn, UserButton, UserProfile, etc.) from your own CSS. Use when a user asks to restyle, rebrand, theme, or match Clerk components to their design system: changing colors, fonts, radius, spacing, dark mode, hover/focus states, or restyling a specific element. Covers the two stable hooks — `--cl-*` design-token variables and `.cl-*` slot classes with `data-*` variant attributes — all in plain CSS. |
Clerk's prebuilt components ship as static CSS with a stable, public contract. You style them from your own stylesheet — there is no styling prop and no build step. Two hooks, and nothing else about the internal DOM is stable:
--cl-*design-token variables — the theming surface (color, radius, spacing, typography). Override these to retheme every component at once..cl-*slot classes +data-*variant attributes — the fine-grained surface. Target a specific element and, optionally, a specific variant of it.
- Brand color, radius, spacing, or font across the whole UI → override a token variable. Start here; one line covers most requests.
- A token doesn't reach the exact element/variant the user wants → override the
.cl-*slot class (narrow it with adata-*attribute if needed). - Dark mode → flip
color-scheme, don't re-declare colors (see below). - Hover / focus / active states → override the slot class, but match Clerk's gating (see Hover and interaction states) — a bare
:hoveris a bug on touch.
Set --cl-* variables anywhere that cascades to the component. :root applies them everywhere.
Color tokens are light-dark() pairs. Every color token carries both a light and a dark value in one declaration, e.g. --cl-foreground: light-dark(black, white). Override a color token with a light-dark(<light>, <dark>) pair, not a flat color, so it keeps responding to the color scheme:
:root {
/* color tokens: always a light-dark() pair */
--cl-foreground: light-dark(black, white);
--cl-background: light-dark(white, #0a0a0a);
--cl-primary: light-dark(#6c47ff, #a68cff);
--cl-primary-foreground: light-dark(white, #0a0a0a);
/* non-color tokens: plain values */
--cl-radius-md: 0.5rem;
--cl-spacing: 0.25rem;
}Setting a color token to a flat value (--cl-primary: #6c47ff) is legal but pins that color in both light and dark — only do it when you genuinely want one color regardless of scheme. For anything that should adapt, keep the pair.
Every element reading a token updates — no rebuild, no prop. Scope to a subtree by setting the variable on a wrapper instead of :root:
.checkout-flow {
--cl-primary: light-dark(#0f9d58, #34d399);
}Common tokens — colors (all light-dark()): --cl-foreground, --cl-background, --cl-primary, --cl-primary-foreground, --cl-muted-foreground, --cl-destructive, --cl-border. Non-color: --cl-radius-md, --cl-radius-full, --cl-spacing.
Color tokens are defined with light-dark(), so each token already carries both a light and a dark value. You don't re-declare every color for dark mode — you flip the CSS color-scheme and the right branch resolves automatically.
/* Force dark on Clerk's root (or any ancestor, or your app root). */
.cl-root {
color-scheme: dark;
}By default color-scheme is light dark, so Clerk already follows the user's OS preference with no work. To retint a single token instead of picking a mode, override its whole light-dark() pair:
:root {
--cl-primary: light-dark(#5b21b6, #c4b5fd);
}Every public element carries a stable .cl-<slot> class (for example .cl-button, .cl-card, .cl-input). Variants are exposed as data-* attributes on that same element, so you target a variant with an attribute selector — no memorizing compound class names:
/* every button */
.cl-button {
font-weight: 600;
}
/* only the primary variant */
.cl-button[data-variant='primary'] {
background-color: var(--cl-primary);
}
/* only the small size */
.cl-button[data-size='sm'] {
font-size: 13px;
}Inspect an element to confirm its slot class and available data-* attributes. The .cl-* class and data-* attributes are the contract; the atomic hashed classes next to them (x1a2b3c…) are StyleX internals and must never be targeted.
Do not write a bare .cl-button:hover. Clerk gates its own hover paints behind @media (hover: hover) so they don't stick on touchscreens (on a phone, a bare :hover fires on tap and then stays applied until you tap elsewhere). Override hover the same way so behavior matches across devices:
@media (hover: hover) {
.cl-button[data-variant='primary']:hover {
background-color: #49247a;
}
}:focus-visible and :active are not gated — apply them directly.
Note
Prefer :focus-visible over :focus. :focus fires on mouse clicks and taps too, leaving a focus ring stuck on the button after a pointer interaction. :focus-visible only shows the ring when the browser judges it useful — keyboard and other non-pointer navigation — which is what Clerk's own components do. Reserve :focus for the rare case where you truly want a ring on every interaction.
.cl-button:focus-visible {
outline: 2px solid var(--cl-primary);
outline-offset: 2px;
}
.cl-button:active {
transform: translateY(1px);
}Rule of thumb: :hover → wrap in @media (hover: hover); everything else → apply directly.
Clerk ships its styles inside a CSS @layer, and layered styles lose to unlayered ones — so any rule you write in a plain stylesheet already wins by default. You rarely need to think about specificity.
To place Clerk relative to other layers (e.g. beneath a utility framework), import Clerk's stylesheet into a named layer and order the layers yourself:
@layer clerk, components, utilities; /* later layers win */
@import '@clerk/clerk-js/styles.css' layer(clerk);Now anything in components or utilities overrides Clerk, while unlayered rules still beat all three.
- Style in CSS. There is no styling prop — never introduce one to answer a styling request.
- Target only
.cl-*slot classes and theirdata-*attributes. Never target the atomicx…hashed classes. - Prefer a token override over a slot override when both work — it's more durable and covers more surface.
- For dark mode, flip
color-scheme; don't hand-write a second set of colors. - For
:hover, always wrap in@media (hover: hover). Apply:focus-visibleand:activedirectly.