Clerk's <SignIn /> drops into a page in one line and looks like Clerk. The
usual fix is to copy your brand colours into the appearance prop:
<ClerkProvider appearance={{ variables: { colorPrimary: "#4f46e5" } }}>
That works until you add dark mode, change the palette, or let a customer pick a theme. Every copied value is a second source of truth, and it only has one mode.
Pass the variable, not the value
Clerk's appearance variables accept CSS custom properties. Hand it
var(--primary) and the browser resolves it at paint time, so Clerk follows
whatever your stylesheet says right now, including the dark block:
// src/lib/auth/appearance.ts
export const clerkAppearance = {
variables: {
colorPrimary: "var(--primary)",
colorPrimaryForeground: "var(--primary-foreground)",
colorDanger: "var(--destructive)",
colorForeground: "var(--foreground)",
colorNeutral: "var(--foreground)",
colorMutedForeground: "var(--muted-foreground)",
colorBackground: "var(--card)",
colorInput: "var(--background)",
colorInputForeground: "var(--foreground)",
colorRing: "var(--ring)",
fontFamily: "var(--font-body, inherit)",
borderRadius: "var(--radius-md, 0.375rem)",
},
};
If your design system follows shadcn's names, every one of those already exists in both modes. Toggle the theme and the sign-in card flips with the page, because nothing in it was ever a literal.
The border trap
The obvious move is colorBorder: "var(--border)". Do not. Clerk draws its
borders from colorBorder at a low opacity, so an already light hairline
colour turns almost invisible, most visibly in dark mode.
Leave colorBorder out. Clerk then derives borders from colorNeutral, and if
that is your text colour, a few percent of it lands close to your design's
hairline in both modes. Check it against a real input on your page before you
fight it.
Browser support
Clerk mixes hover and border shades from these values with color-mix() and
relative colour syntax. Clerk's docs list Chrome 111, Firefox 113 and Safari
16.2 as the floor for that. If you must support older browsers, you are back to
literal values, one set per mode.
Start from Clerk's plain theme
Clerk's default look adds its own touches on top of your variables: a gradient sheen on the primary button and soft shadows. Next to your own flat buttons they read as someone else's component. Clerk ships a plain base for exactly this:
export const clerkAppearance = {
theme: "simple",
variables: { /* as above */ },
};
With simple, what you see is your variables and nothing else.
Let your card be the card
Clerk renders its own card with its own shadow and radius. Inside your page that is a card in a card. Turn it off and wrap Clerk in your own component:
options: { elevation: "flush", logoPlacement: "none" },
// Your page already says "By continuing you agree to the Terms". Leave
// termsPageUrl and privacyPageUrl unset, or Clerk prints the links again.
elements: {
rootBox: { width: "100%" },
cardBox: { width: "100%", maxWidth: "100%" },
},
<Card>
<SignIn fallbackRedirectUrl="/dashboard" />
</Card>
options is Clerk Core 3's name for what was layout.
Style objects, not class names
elements accepts class names, and a Tailwind class there looks like it should
work. It often does not: Clerk's styles are not in a cascade layer, and
unlayered CSS beats anything in Tailwind's layers regardless of specificity. A
style object sets the property directly and wins:
elements: {
formFieldInput: { minHeight: "2.5rem" }, // match your own inputs
formButtonPrimary: { minHeight: "2.5rem", boxShadow: "none" },
},
Settings pages without Clerk's chrome
<UserProfile /> has its own sidebar. If your app already has settings tabs,
render one Clerk page per tab and hide the sidebar:
<UserProfile routing="hash" appearance={{
elements: { navbar: { display: "none" }, navbarMobileMenuRow: { display: "none" } },
}}>
<UserProfile.Page label="security" />
<UserProfile.Page label="account" />
</UserProfile>
Listing a built-in page first makes it the one that opens. routing="hash"
because a plain /settings/security route has no catch-all segment for Clerk's
path routing.
Check it
- Toggle light and dark on
/sign-in: the card, inputs, buttons and links all change, with no reload. - Swap your design's
--primary: the Continue button follows. - Grep
src/lib/auth/appearance.tsfor#: nothing. - Tab through the form: the focus ring is your
--ring.