Selection Field

Native Select

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

stable
select native dropdown html

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

PropTypeDefaultDescription
type'native-select'-Field type (required)
labelstring-Label text
placeholderstring-Placeholder option text
helperTextstring-Help text below field
optionsOption[]-Array of options (required)
schemaz.ZodType | ValidationRules-Validation: a Zod schema or serializable JSON rules (required — fluent methods like .required() populate it for you)
valuestring-Initial selected value
size'xs' | 'sm' | 'md' | 'lg' | 'xl'-Declared but not applied by the native-select renderer
columnsPartial<Record<Breakpoint, number>>-Column span (1–12) per breakpoint, in manual layout
disabledboolean | function | ConditionGroupfalseDisable the select
hiddenboolean | function | ConditionGroup | ResponsivefalseHide the field

Native Select vs Styled Select

Use Native SelectUse Select
Mobile-first appsCustom styling needed
Maximum compatibilityRich interactions
Plain text optionsOptions 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