Skip to content

Instantly share code, notes, and snippets.

@alexcarpenter
Last active July 17, 2026 19:27
Show Gist options
  • Select an option

  • Save alexcarpenter/9bff32d2a1dc4cae54c8dce9493caef9 to your computer and use it in GitHub Desktop.

Select an option

Save alexcarpenter/9bff32d2a1dc4cae54c8dce9493caef9 to your computer and use it in GitHub Desktop.
customizing-clerk/SKILL.md
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.

Customizing Clerk components with 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:

  1. --cl-* design-token variables — the theming surface (color, radius, spacing, typography). Override these to retheme every component at once.
  2. .cl-* slot classes + data-* variant attributes — the fine-grained surface. Target a specific element and, optionally, a specific variant of it.

Decision: which hook

  • 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 a data-* 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 :hover is a bug on touch.

Override design-token variables

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.

Dark mode (color-scheme, not re-declaring colors)

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);
}

Override with slot classes

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.

Hover and interaction states

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.

Precedence and cascade layers

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.

Rules

  • Style in CSS. There is no styling prop — never introduce one to answer a styling request.
  • Target only .cl-* slot classes and their data-* attributes. Never target the atomic x… 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-visible and :active directly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment