Selection Field

Native Select

Native HTML select dropdown for simple selection needs with maximum browser compatibility.

View as Markdown
stable
selectnativedropdownhtml

Interactive demo of native select field

Where are you located?

/**
 * Native Select Field Demo - Interactive examples of native select fields
 */

import { Form, FormBuilder } from '@saastro/forms';

import { FormProvider } from '@/components/FormProvider';
import { TooltipProvider } from '@/components/ui/tooltip';

const config = FormBuilder.create('native-select-demo')
  .layout('manual')
  .columns(12)
  .addField('country', (f) =>
    f
      .type('native-select')
      .label('Country')
      .placeholder('Select a country')
      .helperText('Where are you located?')
      .options([
        { label: 'United States', value: 'us' },
        { label: 'Canada', value: 'ca' },
        { label: 'Mexico', value: 'mx' },
        { label: 'United Kingdom', value: 'uk' },
        { label: 'Germany', value: 'de' },
        { label: 'France', value: 'fr' },
      ])
      .required('Please select a country')
      .columns({ default: 12, md: 6 }),
  )
  .addField('language', (f) =>
    f
      .type('native-select')
      .label('Language')
      .placeholder('Select language')
      .options([
        { label: 'English', value: 'en' },
        { label: 'Spanish', value: 'es' },
        { label: 'French', value: 'fr' },
        { label: 'German', value: 'de' },
      ])
      .value('en')
      .required()
      .columns({ default: 12, md: 6 }),
  )
  .addField('size', (f) =>
    f
      .type('native-select')
      .label('T-Shirt Size')
      .placeholder('Select size')
      .options([
        { label: 'Extra Small (XS)', value: 'xs' },
        { label: 'Small (S)', value: 's' },
        { label: 'Medium (M)', value: 'm' },
        { label: 'Large (L)', value: 'l' },
        { label: 'Extra Large (XL)', value: 'xl' },
      ])
      .required()
      .columns({ default: 12 }),
  )
  .addStep('main', ['country', 'language', 'size'])
  .build();

export default function NativeSelectDemo() {
  const handleSubmit = (data: Record<string, unknown>) => {
    console.log('Form submitted:', data);
    alert('Form submitted! Check console for data.');
  };

  return (
    <TooltipProvider>
      <FormProvider>
        <Form config={config} onSubmit={handleSubmit} className="space-y-4" />
      </FormProvider>
    </TooltipProvider>
  );
}

Overview

The native select field uses the browser’s built-in <select> element. It’s ideal when you need maximum compatibility, mobile-friendly dropdowns, or when the styled Select is overkill. The first option always renders with an empty value and shows the placeholder text.

Default placeholder. When placeholder is omitted, the first option falls back to the built-in English default 'Enter a value...'. Set a placeholder per field or override the defaults globally with setDefaultMessages — see the Internationalization guide.

Usage

Basic Native Select

import { FormBuilder } from '@saastro/forms';

const config = FormBuilder.create('form')
  .addField('country', (f) =>
    f
      .type('native-select')
      .label('Country')
      .placeholder('Select a country')
      .options([
        { label: 'United States', value: 'us' },
        { label: 'Canada', value: 'ca' },
        { label: 'Mexico', value: 'mx' },
      ])
      .required('Please select a country'),
  )
  .addStep('main', ['country'])
  .build();

With Default Value

.addField('language', (f) =>
  f.type('native-select')
    .label('Language')
    .options([
      { label: 'English', value: 'en' },
      { label: 'Spanish', value: 'es' },
      { label: 'French', value: 'fr' },
    ])
    .value('en')
    .required()
)

With Many Options

Options render as a flat list — <optgroup> grouping is not supported.

.addField('timezone', (f) =>
  f.type('native-select')
    .label('Timezone')
    .options([
      { label: 'America/New_York', value: 'america-new-york' },
      { label: 'America/Los_Angeles', value: 'america-la' },
      { label: 'Europe/London', value: 'europe-london' },
      { label: 'Europe/Paris', value: 'europe-paris' },
      { label: 'Asia/Tokyo', value: 'asia-tokyo' },
    ])
    .required()
)

JSON Configuration

{
  "type": "native-select",
  "label": "Country",
  "placeholder": "Select a country",
  "options": [
    { "label": "United States", "value": "us" },
    { "label": "Canada", "value": "ca" },
    { "label": "Mexico", "value": "mx" }
  ],
  "schema": { "required": true }
}

Props

Prop Type Default Description
type 'native-select' - Field type (required)
label string - Label text
placeholder string - Placeholder option text
helperText string - Help text below field
options Option[] - Array of options (required)
schema z.ZodType | ValidationRules - Validation: a Zod schema or serializable JSON rules (required — fluent methods like .required() populate it for you)
value string - Initial selected value
size 'xs' | 'sm' | 'md' | 'lg' | 'xl' - Declared but not applied by the native-select renderer
columns Partial<Record<Breakpoint, number>> - Column span (1–12) per breakpoint, in manual layout
disabled boolean | function | ConditionGroup false Disable the select
hidden boolean | function | ConditionGroup | Responsive false Hide the field
loadOptions (search?: string) => Promise<Option[]> - Async loader, called once on mount. While it resolves, options is the fallback (pass [] for a purely async field). Not serializable — code-defined forms only.
optionsSource RemoteOptionsSource - Serializable async source: fetches url, walks path, maps each item via labelKey/valueKey. Survives the builder/Hub config. If both are set, loadOptions wins.

Native Select vs Styled Select

Use Native Select Use Select
Mobile-first apps Custom styling needed
Maximum compatibility Rich interactions
Plain text options Options with icons

Validation

Required Selection

.addField('size', (f) =>
  f.type('native-select')
    .label('Size')
    .options([
      { label: 'Small', value: 's' },
      { label: 'Medium', value: 'm' },
      { label: 'Large', value: 'l' },
    ])
    .required('Please select a size')
)

Styling

Custom Classes

.addField('country', (f) =>
  f.type('native-select')
    .label('Country')
    .options([
      { label: 'United States', value: 'us' },
      { label: 'Canada', value: 'ca' },
    ])
    .required()
    .classNames({
      wrapper: 'bg-muted/30 p-3 rounded-lg',
      input: 'border-primary bg-background',
      label: 'text-sm font-medium',
      error: 'text-destructive text-sm',
    })
)

Responsive Layout

Per-field columns apply when the form uses manual layout — see the Layout guide.

.addField('language', (f) =>
  f.type('native-select')
    .label('Language')
    .options([
      { label: 'English', value: 'en' },
      { label: 'Spanish', value: 'es' },
    ])
    .optional()
    .columns({ default: 12, md: 6, lg: 4 })
)

JSON Styling

{
  "type": "native-select",
  "label": "Country",
  "options": [
    { "label": "United States", "value": "us" },
    { "label": "Canada", "value": "ca" }
  ],
  "schema": { "required": true },
  "wrapper_className": "bg-muted/30 p-3 rounded-lg",
  "input_className": "border-primary",
  "label_className": "text-sm font-medium",
  "columns": { "default": 12, "md": 6 }
}
  • Select - Styled select with custom trigger and items
  • Combobox - Searchable select
  • Radio - Visible options

Accessibility

  • Native <select> element for full accessibility
  • Works with all assistive technologies
  • Keyboard navigation built-in
  • Mobile-friendly with native picker UI