Theme
Add theme selection with system, light, and dark modes to your Solid/Zaidan authentication flow.
The theme plugin adds theme selection to your authentication UI. Users can switch between system, light, and dark themes from the user button dropdown and account settings.
Setup
Install the UI plugin
Run the shadcn CLI to install the theme plugin factory, preference helpers, and copied Theme UI components:
npx shadcn@latest add https://better-auth-ui.com/r/solid/theme.jsonRegister the UI plugin
The plugin is theme-library agnostic. The recommended Zaidan setup is to use the copied theme helper from src/lib/theme.ts: run themeScript before Solid hydrates, sync the document preference in your provider shell, then register themePlugin() so the plugin's slot components read the persisted theme state.
import { themeScript } from "@/lib/theme"
export const Route = createRootRoute({
head: () => ({
scripts: [{ children: themeScript }]
})
})import { onCleanup, onMount } from "solid-js"
import { AuthProvider } from "@/components/auth/auth-provider"
import { themePlugin } from "@/lib/auth/theme-plugin"
import { authClient } from "@/lib/auth-client"
import { syncDocumentThemePreference } from "@/lib/theme"
export function Providers(props: { children?: JSX.Element }) {
onMount(() => {
const cleanup = syncDocumentThemePreference()
onCleanup(cleanup)
})
return (
<AuthProvider
authClient={authClient}
plugins={[
themePlugin()
]}
>
{props.children}
</AuthProvider>
)
}When the plugin is registered, <UserButton /> renders the Theme menu item through userMenuItems, and account settings render <Appearance /> through accountCards.
Or pass static theme state
If you manage the theme with a custom Solid signal or controller, pass both theme and setTheme from that stateful source. The plugin receives the current value from the parent and the slot components call your setter when users change themes.
import { createSignal } from "solid-js"
import type { ThemeMode } from "@/lib/theme"
const [theme, setTheme] = createSignal<ThemeMode>("system")
<AuthProvider
authClient={authClient}
plugins={[
themePlugin({
theme: theme(),
setTheme: (nextTheme) => setTheme(nextTheme as ThemeMode),
themes: ["system", "light", "dark"]
})
]}
>
{props.children}
</AuthProvider>Use either the copied helper defaults or an explicit theme/setTheme pair when you need a custom controller.
Components
These previews use local theme state only. They do not call Better Auth endpoints or require a live session.
<UserButton />
The Theme menu item is automatically rendered in <UserButton /> when themePlugin() is registered.
Usage
import { AuthProvider } from "@/components/auth/auth-provider"
import { UserButton } from "@/components/auth/user/user-button"
import { themePlugin } from "@/lib/auth/theme-plugin"
import { authClient } from "@/lib/auth-client"
export function ThemeUserButtonDemo() {
return (
<AuthProvider authClient={authClient} plugins={[themePlugin()]}>
{() => <UserButton />}
</AuthProvider>
)
}<Appearance />
The <Appearance /> card is automatically rendered in account settings when themePlugin() is registered and the account page renders plugin-contributed accountCards.
Usage
import { Appearance } from "@/components/auth/theme/appearance"
export function AppearanceDemo() {
return <Appearance />
}Props
Prop
Type
Options
Prop
Type
Localization
Prop
Type
The copied Zaidan Theme components resolve labels from themePlugin({ localization }) and fall back to the default Theme localization.
Last updated on