Concepts
Customising the components
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.
<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.
// 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 |
appearance={{
accentColor: '#5b3df5',
borderRadius: 'large',
fontFamily: 'inherit',
}}
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:
<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:
<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 |
<SignIn appearance={{ signInTitle: 'Welcome back', signInSubtitle: 'Sign in to continue.' }} />
<SignIn appearance={{ signInTitle: null }} /> {/* no heading */}
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:
<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:
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:
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:
<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:
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.
Updated at, Wednesday, October 21, 2026