Skip to content

Select

A versatile dropdown select component with support for option groups, validation, and multiple selection. Perfect for forms, filters, and data selection. Built for Pulse Framework with full reactivity support.

Import

tsx
import { Select, Pulse } from '@odyssee/components';

Basic Usage

Code Éditable
Résultat

With Label and Hint

Add labels and helper text for better UX.

Code Éditable
Résultat

Sizes

Five size options control padding and text size: xs, sm, md, lg, and xl.

Code Éditable
Résultat

Required Field

Mark fields as required with visual indicators.

Code Éditable
Résultat

With Error

Display validation errors.

Code Éditable
Résultat

With Option Groups

Organize options into labeled groups.

Code Éditable
Résultat

Disabled Options

Disable specific options.

Code Éditable
Résultat

Disabled State

Disable the entire select.

Code Éditable
Résultat

Multiple Selection

Allow selecting multiple options.

tsx
const multiSelect = (
  <Select
    label="Tags"
    multiple={true}
    placeholder="Select tags"
    options={[
      { value: 'javascript', label: 'JavaScript' },
      { value: 'typescript', label: 'TypeScript' },
      { value: 'react', label: 'React' },
      { value: 'vue', label: 'Vue' },
      { value: 'angular', label: 'Angular' }
    ]}
  />
);

Reactive Value

Control select value with Pulse signals.

tsx
const country = Pulse.signal('us');

const reactiveSelect = (
  <div>
    <Select
      label="Country"
      value={country}
      options={[
        { value: 'us', label: 'United States' },
        { value: 'uk', label: 'United Kingdom' },
        { value: 'fr', label: 'France' },
        { value: 'de', label: 'Germany' }
      ]}
      onChange={(val) => country(val)}
    />
    
    <div class="mt-4">
      <p class="text-sm text-gray-600">Selected: {country()}</p>
    </div>
  </div>
);

Dependent Selects

Create cascading select dropdowns.

tsx
const DependentSelects = () => {
  const country = Pulse.signal('');
  const city = Pulse.signal('');

  const cities = Pulse.computed(() => {
    const countryVal = country();
    if (countryVal === 'us') {
      return [
        { value: 'nyc', label: 'New York' },
        { value: 'la', label: 'Los Angeles' },
        { value: 'chicago', label: 'Chicago' }
      ];
    } else if (countryVal === 'uk') {
      return [
        { value: 'london', label: 'London' },
        { value: 'manchester', label: 'Manchester' },
        { value: 'birmingham', label: 'Birmingham' }
      ];
    }
    return [];
  });

  return (
    <div class="space-y-4">
      <Select
        label="Country"
        value={country}
        options={[
          { value: 'us', label: 'United States' },
          { value: 'uk', label: 'United Kingdom' }
        ]}
        onChange={(val) => {
          country(val);
          city(''); // Reset city when country changes
        }}
      />

      <Select
        label="City"
        value={city}
        options={cities()}
        disabled={!country()}
        onChange={(val) => city(val)}
      />
    </div>
  );
};

Form Validation

Integrate with form validation.

tsx
const RegistrationForm = () => {
  const role = Pulse.signal('');
  const error = Pulse.signal('');

  const validate = () => {
    if (!role()) {
      error('Please select a role');
      return false;
    }
    error('');
    return true;
  };

  const handleSubmit = (e: Event) => {
    e.preventDefault();
    if (validate()) {
      console.log('Form submitted with role:', role());
    }
  };

  return (
    <form onsubmit={handleSubmit} class="space-y-4">
      <Select
        label="Role"
        value={role}
        onChange={(val) => {
          role(val);
          validate();
        }}
        error={error()}
        placeholder="Select your role"
        required
        options={[
          { value: 'developer', label: 'Developer' },
          { value: 'designer', label: 'Designer' },
          { value: 'manager', label: 'Manager' },
          { value: 'other', label: 'Other' }
        ]}
      />
      <Button type="submit">Register</Button>
    </form>
  );
};

Complete Example

Here's a comprehensive user profile form with multiple selects:

tsx
import { Select, Input, Button, Alert, Pulse } from '@odyssee/components';

const UserProfileForm = () => {
  const profile = Pulse.signal({
    country: '',
    state: '',
    timezone: '',
    language: 'en'
  });

  const errors = Pulse.signal({});
  const isSubmitting = Pulse.signal(false);
  const success = Pulse.signal(false);

  const countries = [
    { value: 'us', label: 'United States' },
    { value: 'uk', label: 'United Kingdom' },
    { value: 'ca', label: 'Canada' },
    { value: 'au', label: 'Australia' }
  ];

  const states = Pulse.computed(() => {
    if (profile().country === 'us') {
      return [
        { value: 'ny', label: 'New York' },
        { value: 'ca', label: 'California' },
        { value: 'tx', label: 'Texas' }
      ];
    }
    return [];
  });

  const timezones = [
    { value: 'est', label: 'Eastern Time (EST)', group: 'US' },
    { value: 'cst', label: 'Central Time (CST)', group: 'US' },
    { value: 'mst', label: 'Mountain Time (MST)', group: 'US' },
    { value: 'pst', label: 'Pacific Time (PST)', group: 'US' },
    { value: 'gmt', label: 'Greenwich Mean Time (GMT)', group: 'Europe' },
    { value: 'cet', label: 'Central European Time (CET)', group: 'Europe' }
  ];

  const languages = [
    { value: 'en', label: 'English' },
    { value: 'es', label: 'Spanish' },
    { value: 'fr', label: 'French' },
    { value: 'de', label: 'German' },
    { value: 'ja', label: 'Japanese' }
  ];

  const validate = () => {
    const newErrors = {};
    if (!profile().country) {
      newErrors.country = 'Country is required';
    }
    if (!profile().timezone) {
      newErrors.timezone = 'Timezone is required';
    }
    errors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = async (e: Event) => {
    e.preventDefault();
    
    if (!validate()) {
      return;
    }

    isSubmitting(true);

    try {
      await new Promise(resolve => setTimeout(resolve, 1500));
      console.log('Profile updated:', profile());
      success(true);
      setTimeout(() => success(false), 3000);
    } catch (err) {
      errors({ ...errors(), form: 'Failed to update profile' });
    } finally {
      isSubmitting(false);
    }
  };

  return (
    <div class="max-w-2xl">
      {success() && (
        <Alert
          color="success"
          dismissible={true}
          onDismiss={() => success(false)}
          className="mb-4"
        >
          Profile updated successfully!
        </Alert>
      )}

      <form onsubmit={handleSubmit} class="space-y-4">
        <Select
          label="Country"
          value={profile().country}
          onChange={(val) => {
            profile({ ...profile(), country: val, state: '' });
            errors({ ...errors(), country: '' });
          }}
          error={errors().country}
          placeholder="Select your country"
          options={countries}
          required
          disabled={isSubmitting()}
        />

        {profile().country === 'us' && (
          <Select
            label="State"
            value={profile().state}
            onChange={(val) => profile({ ...profile(), state: val })}
            placeholder="Select your state"
            options={states()}
            disabled={isSubmitting()}
          />
        )}

        <Select
          label="Timezone"
          value={profile().timezone}
          onChange={(val) => {
            profile({ ...profile(), timezone: val });
            errors({ ...errors(), timezone: '' });
          }}
          error={errors().timezone}
          placeholder="Select your timezone"
          options={timezones}
          required
          disabled={isSubmitting()}
        />

        <Select
          label="Preferred Language"
          value={profile().language}
          onChange={(val) => profile({ ...profile(), language: val })}
          options={languages}
          disabled={isSubmitting()}
        />

        <div class="flex gap-2">
          <Button
            type="submit"
            color="primary"
            loading={isSubmitting()}
            disabled={isSubmitting()}
          >
            Save Changes
          </Button>
          <Button
            variant="outline"
            onClick={() => profile({
              country: '',
              state: '',
              timezone: '',
              language: 'en'
            })}
            disabled={isSubmitting()}
          >
            Reset
          </Button>
        </div>
      </form>
    </div>
  );
};

Props

PropTypeDefaultDescription
valuestring | string[] | Signal<string | string[]>-Selected value(s)
optionsSelectOption[]RequiredArray of options
placeholderstring"Select an option"Placeholder text
labelstring-Label text
hintstring-Helper text below select
errorstring-Error message
size"xs" | "sm" | "md" | "lg" | "xl""md"Select size
multiplebooleanfalseEnable multiple selection
disabledbooleanfalseDisable select
requiredbooleanfalseMark as required
onChange(value: string | string[]) => void-Change event handler
classNamestring-Additional CSS classes
idstringAuto-generatedHTML id attribute

SelectOption Type

tsx
interface SelectOption {
  value: string | number;
  label: string;
  group?: string;
  disabled?: boolean;
}

Accessibility

The Select component follows accessibility best practices:

  • ✅ Proper label association with for attribute
  • ✅ ARIA attributes for errors and descriptions
  • ✅ Required field indicators
  • ✅ Keyboard navigation support
  • ✅ Focus management
  • ✅ Screen reader friendly error messages
  • ✅ Option groups with proper semantics

ARIA Attributes

tsx
const accessibleSelect = (
  <Select
    label="Country"
    aria-label="Select country"
    aria-describedby="country-hint"
    aria-invalid={hasError}
    options={countries}
  />
);

Best Practices

✅ Do

  • Provide clear labels and hints
  • Group related options
  • Show validation errors clearly
  • Use appropriate placeholder text
  • Disable options when needed
  • Handle loading states
  • Validate on change for better UX
tsx
// Good: Clear labels and validation
const goodSelect = (
  <Select
    label="Subscription Plan"
    hint="You can change your plan anytime"
    placeholder="Choose a plan"
    required
    options={planOptions}
  />
);

❌ Don't

  • Don't use select for very long lists (use search/autocomplete)
  • Don't forget to handle empty states
  • Don't use vague placeholders like "Select"
  • Don't hide critical options in groups
  • Don't forget to reset dependent selects
tsx
// Bad: Too many ungrouped options
const badSelect = (
  <Select
    placeholder="Select"
    options={[...100 countries]}
  />
);

// Better: Use groups or autocomplete
const betterSelect = (
  <Select
    label="Country"
    placeholder="Select your country"
    options={[
      { value: 'us', label: 'United States', group: 'North America' },
      { value: 'ca', label: 'Canada', group: 'North America' },
      // Grouped by region...
    ]}
  />
);

Use Cases

Filter Dropdown

tsx
const FilterSelect = () => (
  <Select
    label="Filter by Status"
    placeholder="All statuses"
    options={[
      { value: 'all', label: 'All' },
      { value: 'active', label: 'Active' },
      { value: 'inactive', label: 'Inactive' },
      { value: 'pending', label: 'Pending' }
    ]}
  />
);

Language Selector

tsx
const LanguageSelector = () => (
  <Select
    label="Language"
    options={[
      { value: 'en', label: '🇺🇸 English' },
      { value: 'es', label: '🇪🇸 Español' },
      { value: 'fr', label: '🇫🇷 Français' },
      { value: 'de', label: '🇩🇪 Deutsch' }
    ]}
  />
);

Priority Selector

tsx
const PrioritySelect = () => (
  <Select
    label="Priority"
    options={[
      { value: 'low', label: '🟢 Low' },
      { value: 'medium', label: '🟡 Medium' },
      { value: 'high', label: '🔴 High' }
    ]}
  />
);

Styling & Theming

All select styles use Tailwind CSS classes and support dark mode automatically.

Custom Styling

tsx
const customSelect = (
  <Select
    className="font-semibold"
    options={options}
  />
);

TypeScript

Full TypeScript support with complete type definitions:

tsx
import type { SelectProps, SelectOption } from '@odyssee/components';

const options: SelectOption[] = [
  { value: 'opt1', label: 'Option 1' },
  { value: 'opt2', label: 'Option 2' }
];

const props: SelectProps = {
  label: 'Select',
  options: options,
  onChange: (value: string | string[]) => {
    console.log('Selected:', value);
  }
};

const select = <Select {...props} />;

Version: 1.0.0
Last Updated: January 2025

Released under the MIT License.