@remcostoeten/auth-drawer
Authentication UI, ready to adapt.
A configurable React authentication drawer and modal. Bring your own auth backend; keep the OAuth flows, forms, states, and polished presentation in one reusable UI primitive.
What it replaces
Use Auth Drawer when authentication is already handled by a provider or API, but the product still needs a cohesive sign-in surface. It keeps this high-friction, highly repeated UI out of every individual app.
Live demo
The real drawer, running on this page against the mock adapter. Every flow works: sign in, register, OAuth, and password reset.
Quick start
1import { AuthDrawer, AuthProvider } from '@remcostoeten/auth-drawer'2import { createBetterAuthAdapter } from '@remcostoeten/auth-drawer/adapters/better-auth'34const adapter = createBetterAuthAdapter({ client })56export function Auth() {7 return (8 <AuthProvider adapter={adapter}>9 <AuthDrawer adapter={adapter} />10 </AuthProvider>11 )12}Integration shape
Map existing auth client to typed adapter once.
Place AuthProvider and AuthDrawer near application root.
Use hook or trigger API from navigation, paywalls, or protected actions.
API examples
AuthDrawer renders the surface, useAuth opens it and reads session state, and the adapter connects those calls to your existing auth client. Your auth client still owns credentials, sessions, and requests. The whole surface is typed: config keys, provider names, adapter methods, and error codes autocomplete in the editor, so most integrations never need the docs open.
Start with the provider and drawer near your app root.
1import { AuthDrawer } from '@remcostoeten/auth-drawer'23export function Auth() {4 return <AuthDrawer adapter={adapter} />5}Pass the adapter once when the drawer needs shared auth state.
1import { AuthDrawer, AuthProvider } from '@remcostoeten/auth-drawer'23export function Auth() {4 return (5 <AuthProvider adapter={adapter}>6 <AuthDrawer adapter={adapter} />7 </AuthProvider>8 )9}Use the hook when a button or protected action owns the trigger.
1import { useAuth } from '@remcostoeten/auth-drawer'23export function SignInButton() {4 const { open } = useAuth()56 return <button onClick={() => open('sign-in')}>Sign in</button>7}One optional AuthConfig object, deep merged over sensible defaults. Set only what you change and let intellisense walk you through the rest: ui.auth for providers and form flags, ui.presentation for drawer or modal, ui.copy for every string.
1<AuthDrawer2 adapter={adapter}3 config={{4 ui: {5 auth: {6 providers: ['github', 'google'],7 allowRegister: true,8 emailAutocomplete: { domains: ['company.com'] }9 },10 presentation: { variant: 'modal' }11 }12 }}13/>An adapter is a plain object mapping your auth API to the AuthResult shape. Only signIn is required with the createAdapter helper: the drawer feature-detects signUp, OAuth, and reset methods and renders only the flows you implement. Typed error codes like rate_limited and invalid_credentials pick the right message and retry behavior.
1import { createAdapter } from '@remcostoeten/auth-drawer'23export const adapter = createAdapter({4 id: 'my-api',5 async signIn({ email, password }) {6 const res = await fetch('/api/login', {7 method: 'POST',8 body: JSON.stringify({ email, password })9 })1011 if (!res.ok) {12 const rateLimited = res.status === 4291314 return {15 success: false,16 error: {17 code: rateLimited ? 'rate_limited' : 'invalid_credentials',18 target: 'form',19 message: rateLimited20 ? 'Too many attempts. Wait a moment and try again.'21 : 'Email or password is incorrect.'22 }23 }24 }2526 return { success: true, data: await res.json() }27 }28})<AuthDrawer adapter={adapter} config={config} />adapterRequired. Maps your auth client to the drawer API.configOptional. A typed AuthConfig merged over defaults. Controls copy, OAuth providers, layout, visual styling, and motion.hideTriggerOptional. Hide the built-in trigger when your app opens the drawer itself.<AuthProvider adapter={adapter}>{children}</AuthProvider>adapterRequired. Shares the adapter and session state with nested components.childrenThe part of the app that can call useAuth().const { open, close, user } = useAuth()open(mode?)Open the drawer, optionally on the sign-in or register view.close()Close the drawer from an app-owned action.userRead the current session user exposed by the adapter.