BETTER-AUTH. UI
Concepts

Additional Fields

Render custom user fields in copied Zaidan auth and profile forms.

additionalFields is an AuthProvider config option that declares extra user fields to render on the sign-up form and user profile. Each field describes its data type, label, and optional UI rendering. Better Auth UI then handles rendering, parsing, and submitting the value through signUp.email (sign-up) and updateUser (profile).

Zaidan copies the Solid <AdditionalField /> renderer into your app, so the happy path matches the shadcn API while the UI remains app-owned and customizable.

Define the same fields in your Better Auth server config under user.additionalFields. The UI's additionalFields only controls rendering and form submission — the server still owns persistence and validation.

Usage

Install the composed auth registry entry and the profile registry entry that render additional fields:

npx shadcn@latest add https://better-auth-ui.com/r/solid/auth.json https://better-auth-ui.com/r/solid/user-profile.json

Pass an array of field configurations to the copied <AuthProvider> shell:

import { AuthProvider } from "@/components/auth/auth-provider"

<AuthProvider
  authClient={authClient}
  navigate={navigate}
  additionalFields={[
    {
      name: "bio",
      type: "string",
      label: "Bio",
      inputType: "textarea",
      placeholder: "Tell us a bit about yourself"
    },
    {
      name: "birthday",
      type: "date",
      label: "Birthday",
      signUp: true,
      required: true
    }
  ]}
>
  {children}
</AuthProvider>

Fields default to rendering on the user profile only. Set signUp: true to also render the field on the sign-up form.

Install model

The composed auth/profile registry entries include the additional field renderer when those forms need it. If you're building a smaller custom install, you can install the renderer by itself:

npx shadcn@latest add https://better-auth-ui.com/r/solid/additional-field.json

The registry entry copies:

  • src/components/auth/additional-field.tsx
  • local button, input, and label primitives used by the renderer

After install, adapt validation messages, Solid class styling, and field layouts in your app-owned copy.

Field types

The type controls the data type of the field. The default inputType is inferred from type, but you can override it for a different look.

typeDefault inputTypeSubmitted as
"string""input"string
"number""number"number
"boolean""switch"boolean
"date""date"Date

Input types

Override the visual rendering with inputType. The current Zaidan renderer favors semantic native controls instead of inventing shadcn-only primitives that the Solid registry entry does not ship yet.

inputTypeZaidan rendering behavior
"input"Single-line native input using the copied Zaidan Input
"textarea"Native textarea with Zaidan classes
"number"Native number input with min, max, and step
"slider"Native number fallback; customize the copied renderer for a slider
"switch"Native checkbox fallback
"checkbox"Native checkbox
"select"Native select from options
"combobox"Native select fallback from options; customize for search UI
"date"Native date input
"datetime"Native datetime-local input
"hidden"Hidden input (submitted but not rendered)

Examples

Numeric formatting

number fields can declare Intl.NumberFormatOptions via formatOptions, matching the shared field config. The current Zaidan renderer keeps the control native, so use formatOptions as metadata or customize the copied renderer if you need formatted display text.

{
  name: "hourlyRate",
  type: "number",
  label: "Hourly rate",
  formatOptions: { style: "currency", currency: "USD" }
}

{
  name: "commissionRate",
  type: "number",
  label: "Commission rate",
  formatOptions: { style: "percent", maximumFractionDigits: 2 }
}

Use min, max, and step to bound the native value:

{
  name: "yearsExperience",
  type: "number",
  label: "Years of experience",
  min: 0,
  max: 50,
  step: 1
}

Slider

The shared config supports inputType: "slider", but the current Zaidan registry entry does not ship a Solid slider primitive. The copied renderer falls back to a native number control for number fields. Replace that branch in src/components/auth/additional-field.tsx when your app has a Solid slider component.

{
  name: "budget",
  type: "number",
  label: "Budget",
  inputType: "slider",
  min: 0,
  max: 5000,
  step: 50,
  defaultValue: 1000,
  formatOptions: { style: "currency", currency: "USD" }
}

Select / Combobox

Both accept an options array of { label, value } objects. Zaidan currently renders both as a native <select> so the copied registry entry stays dependency-light.

{
  name: "country",
  type: "string",
  label: "Country",
  inputType: "select",
  options: [
    { label: "United States", value: "us" },
    { label: "Canada", value: "ca" },
    { label: "United Kingdom", value: "gb" }
  ]
}

Use inputType: "combobox" for the same data shape when you plan to replace the native select with your own searchable Solid control.

Prefix / suffix

String and number inputs render inline prefix and suffix text when prefix or suffix is set:

{
  name: "website",
  type: "string",
  label: "Website",
  prefix: "https://",
  suffix: ".com"
}

The current Zaidan renderer uses simple text wrappers next to the input. If your app needs grouped styling, customize the copied renderer with your preferred input-group primitive.

Copy button

Set copyable: true to render a copy-to-clipboard button next to the input. The button copies the input's current value, so it works on editable fields too — though it's most commonly paired with readOnly: true for fields like id:

{
  name: "id",
  type: "string",
  label: "User ID",
  readOnly: true,
  copyable: true
}

Hidden value

inputType: "hidden" submits a value without rendering anything visible. Combine with defaultValue to attach a server-side preset:

{
  name: "referralSource",
  type: "string",
  label: "Referral source",
  inputType: "hidden",
  defaultValue: "demo-app",
  signUp: true
}

Custom rendering

Use render when the native Zaidan renderer is not enough. The function receives the same props as the copied <AdditionalField /> component, so you can keep the same field config while swapping the control.

{
  name: "timezone",
  type: "string",
  label: "Timezone",
  render: ({ name, field, isPending }) => (
    <div class="grid gap-2">
      <label for={name}>{field.label}</label>
      <select id={name} name={name} disabled={isPending} class="z-input">
        <option value="America/New_York">Eastern Time</option>
        <option value="America/Los_Angeles">Pacific Time</option>
      </select>
    </div>
  )
}

Custom validation

Provide a validate callback to run client-side checks before submission. Throw an Error to reject — the message is shown to the user via toast:

{
  name: "nickname",
  type: "string",
  label: "Nickname",
  signUp: true,
  required: true,
  validate: (value) => {
    if (typeof value === "string" && !/^[a-zA-Z0-9_]+$/.test(value)) {
      throw new Error(
        "Nickname must only contain letters, numbers, and underscores"
      )
    }
  }
}

Where fields render

FlagDefaultEffect
signUp: truefalseRender on the sign-up form
profile: falsetrueHide on the user profile
readOnly: truefalseRender but exclude the value from submission

Type reference

Prop

Type

Prop

Type

Last updated on

On this page