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
import { Select, Pulse } from '@odyssee/components';Basic Usage
With Label and Hint
Add labels and helper text for better UX.
Sizes
Five size options control padding and text size: xs, sm, md, lg, and xl.
Required Field
Mark fields as required with visual indicators.
With Error
Display validation errors.
With Option Groups
Organize options into labeled groups.
Disabled Options
Disable specific options.
Disabled State
Disable the entire select.
Multiple Selection
Allow selecting multiple options.
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.
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.
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.
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:
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
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | string[] | Signal<string | string[]> | - | Selected value(s) |
options | SelectOption[] | Required | Array of options |
placeholder | string | "Select an option" | Placeholder text |
label | string | - | Label text |
hint | string | - | Helper text below select |
error | string | - | Error message |
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Select size |
multiple | boolean | false | Enable multiple selection |
disabled | boolean | false | Disable select |
required | boolean | false | Mark as required |
onChange | (value: string | string[]) => void | - | Change event handler |
className | string | - | Additional CSS classes |
id | string | Auto-generated | HTML id attribute |
SelectOption Type
interface SelectOption {
value: string | number;
label: string;
group?: string;
disabled?: boolean;
}Accessibility
The Select component follows accessibility best practices:
- ✅ Proper label association with
forattribute - ✅ 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
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
// 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
// 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
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
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
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
const customSelect = (
<Select
className="font-semibold"
options={options}
/>
);TypeScript
Full TypeScript support with complete type definitions:
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} />;Related Components
- Input - For text input
- Textarea - For multiline input
- Checkbox - For multiple selections
- Radio - For single selections
Version: 1.0.0
Last Updated: January 2025