# Make Clerk's sign-in look native, in light and dark, with CSS variables

> Point Clerk's appearance variables at your design's CSS custom properties so the prebuilt components follow your theme and your dark mode, with no copied hex values.

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:

```tsx
<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:

```ts
// 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:

```ts
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:

```ts
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%" },
},
```

```tsx
<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:

```ts
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:

```tsx
<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.ts` for `#`: nothing.
- Tab through the form: the focus ring is your `--ring`.

---

Agentic Boilerplate: A Next.js repo your agent already knows. $99 once. Lifetime access and updates.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
