---
title: Customising the components
description: Every setting, where it lives, and how to override it from code.
---

Every visual decision has two homes. The **console** is for people who
are not going to open an editor, and it applies everywhere at once. The
**`appearance` prop** is for decisions that belong in your repository,
reviewed with your code and deployed with it.

You do not choose one. Code wins where it speaks, and the console
answers everything it leaves out.

```tsx
<ZevAuthProvider
  publishableKey="pk_live_..."
  appearance={{ accentColor: '#5b3df5', cardStyle: 'flat' }}
>
  <App />
</ZevAuthProvider>
```

That sets two things. Your logo, your wording, your layout and everything
else keep coming from the console.

## Where to put it

| Put it | When |
| --- | --- |
| The console | A designer or a founder should be able to change it without a deploy |
| `appearance` on `<ZevAuthProvider>` | It belongs in your repository, and every screen should match |
| `appearance` on one component | One screen should differ from the rest |

The narrower one wins, so a component's `appearance` beats the
provider's, which beats the console.

```tsx
// Everything matches, except the sign-up page, which is flat.
<ZevAuthProvider publishableKey="pk_live_..." appearance={{ accentColor: '#5b3df5' }}>
  <SignIn />
  <SignUp appearance={{ cardStyle: 'flat' }} />
</ZevAuthProvider>
```

## Everything you can set

Field names are identical in the console and in `appearance`, so there is
nothing to translate.

### The brand

| Field | Values | What it does |
| --- | --- | --- |
| `accentColor` | any hex | Buttons, focus rings, links |
| `colorScheme` | `'system'` `'light'` `'dark'` | Follows the device, or forces one |
| `borderRadius` | `'none'` `'small'` `'medium'` `'large'` | Corners on every control |
| `fontFamily` | `'system'` `'inter'` `'inherit'` | `'inherit'` takes your page's font |
| `logoUrl` | a URL | Above the form, and in every auth email |

```tsx
appearance={{
  accentColor: '#5b3df5',
  borderRadius: 'large',
  fontFamily: 'inherit',
}}
```

> **Note:**
> Contrast is resolved on the server. You give one accent; we work out what
> is readable on it in light and in dark, and the components use that. You
> never have to supply a second colour for text.

### The form itself

| Field | Values | What it does |
| --- | --- | --- |
| `cardStyle` | `'card'` `'flat'` | `'flat'` removes the border, background and padding |
| `contentAlign` | `'center'` `'top'` | Where the form sits vertically in its space |
| `showIdentity` | `true` `false` | Whether your logo and name appear above the form |
| `showAppName` | `true` `false` | Whether your name prints under your logo |

**Blending into your own page.** If you have already designed the panel
the form sits in, our card is a box inside a box:

```tsx
<SignIn appearance={{ cardStyle: 'flat', showIdentity: false }} />
```

That removes our border, background, padding, logo and name, leaving the
fields and buttons to inherit your page.

**Sitting under a fixed header.** A centred form collides with an app
shell's top bar. Start it at the top instead:

```tsx
<SignIn appearance={{ contentAlign: 'top' }} />
```

### The words

| Field | Values | What it does |
| --- | --- | --- |
| `signInTitle` | text, or `null` | `null` prints no heading at all |
| `signInSubtitle` | text, or `null` | Empty by default |
| `signUpTitle` | text, or `null` | |
| `signUpSubtitle` | text, or `null` | |

```tsx
<SignIn appearance={{ signInTitle: 'Welcome back', signInSubtitle: 'Sign in to continue.' }} />
<SignIn appearance={{ signInTitle: null }} />   
```

This is also how you ship a language we do not.

### The layout

| Field | Values | What it does |
| --- | --- | --- |
| `signInLayout` | `'centered'` `'split_left'` `'split_right'` | Where the form sits on the page |
| `asideStyle` | `'gradient'` `'solid'` `'image'` | What the panel beside it is made of |
| `asideColor` | any hex | The panel colour |
| `asideColorDark` | any hex | That colour in dark mode |
| `asideImageUrl` | a URL | The panel banner |
| `asideImageUrlDark` | a URL | That banner in dark mode |
| `asideHeading` | text | On the panel |
| `asideText` | text | Under the heading |

The component prop is `layout`, spelled with hyphens, because that is
what a React prop looks like:

```tsx
<SignIn layout="split-right" />
```

Both work, and the prop wins.

### The behaviour

| Field | Values | What it does |
| --- | --- | --- |
| `passwordResetMode` | `'dedicated'` `'inline'` | Whether the reset gets its own screen |

`'dedicated'` is the default and asks for the address on a screen of its
own, prefilled from the sign-in field if something was typed there.
`'inline'` reuses the sign-in field and refuses when it is empty.

## Light and dark

Set `colorScheme` to `'system'` (the default) and every component follows
the viewer's device. The panel beside a split form takes its own dark
variant, because a colour or a photograph chosen for a light page is
often wrong on a dark one:

```tsx
appearance={{
  asideStyle: 'gradient',
  asideColor: '#5b3df5',       // light mode
  asideColorDark: '#1a1440',   // dark mode
}}
```

Leave the dark one out and the light one is used for both.

Writing your own panel? `useZevTheme()` tells you which one is painting,
resolved the same way the components resolve it:

```tsx
import { SignIn, useZevTheme } from '@zevauth/react';

function Panel() {
  const isDark = useZevTheme();
  return <img src={isDark ? '/hero-dark.jpg' : '/hero-light.jpg'} alt="" />;
}

<SignIn layout="split-right" aside={<Panel />} />
```

## Replacing the panel entirely

`aside` takes any JSX and replaces the console picture and copy:

```tsx
<SignIn
  layout="split-right"
  aside={
    <div style={{ padding: 48, color: 'white' }}>
      <blockquote>"We moved 40,000 accounts in an afternoon."</blockquote>
      <cite>Ada Lovelace, CTO</cite>
    </div>
  }
/>
```

## A worked example

An app shell with its own header, its own panel, and its own dark mode:

```tsx
import { ZevAuthProvider, SignIn, SignUp } from '@zevauth/react';

export function Auth() {
  return (
    <ZevAuthProvider
      publishableKey={import.meta.env.VITE_ZEVAUTH_PUBLISHABLE_KEY}
      appearance={{
        // Our page already has a panel, a logo and a font.
        cardStyle: 'flat',
        showIdentity: false,
        fontFamily: 'inherit',
        contentAlign: 'top',
        accentColor: '#5b3df5',
        signInTitle: 'Welcome back',
        signInSubtitle: 'Sign in to continue.',
      }}
    >
      <MyHeader />
      <main className="my-panel">
        <SignIn secondaryMethods="collapsed" afterSignInUrl="/dashboard" />
      </main>
    </ZevAuthProvider>
  );
}
```

## One thing you cannot set

`showPoweredBy` is not part of `appearance`. Whether "Secured by ZevAuth"
appears is resolved on the server from your plan, and the merge drops any
attempt to set it in code, including from plain JavaScript with no types
to stop it.

Removing it is a paid feature, switched off under **Customisation →
Badge** in the console once your plan includes it. Development
environments always show it, so you never design against a screen
production will not give you.
