ForgeSelect

September 11, 2025 · View on GitHub

Flexible dropdown selection component with unified SSR/client architecture for single-value choices and form integration.

Unified SSR Architecture

The ForgeSelect component uses our revolutionary unified wrapper approach - a single component that works everywhere without any developer configuration:

  • SSR Environment (Next.js): Renders semantic HTML <select> with native options and critical styling
  • Client Environment (Vite): Renders as web component with enhanced UI and search functionality
  • Progressive Enhancement: Seamlessly upgrades from HTML select to web component
  • Graceful Degradation: Falls back to enhanced HTML select if web components fail

Single component that works everywhere - no separate SSR/client versions needed.

Key Features

  • Native Form Integration: Full HTML form submission and validation support
  • Search and Filter: Built-in search functionality for large option sets
  • Custom Styling: Enhanced visual design while maintaining accessibility
  • Icon Support: Icons in options and trigger button
  • Placeholder Support: Customizable placeholder text
  • Validation States: Error, success, and warning visual states
  • Size Variants: Small, medium, and large sizes
  • Accessibility: Complete WCAG 2.1 AA compliance with keyboard navigation
  • Unified SSR: Works seamlessly in both SSR and client environments
  • AI-Ready: Complete AI metadata for intelligent interactions

Basic Usage

Import (Works in any React environment)

import { ForgeSelect } from '@nexcraft/forge/integrations/react';

// Same import works in:
// ✅ Next.js App Router (SSR)
// ✅ Next.js Pages Router (SSR) 
// ✅ Vite (Client-only)
// ✅ CRA (Client-only)
// ✅ Any React SSR framework

Basic Select

// Simple select
<ForgeSelect 
  name="country"
  placeholder="Select country"
  onChange={handleCountryChange}
>
  <option value="us">United States</option>
  <option value="ca">Canada</option>
  <option value="uk">United Kingdom</option>
  <option value="de">Germany</option>
  <option value="fr">France</option>
</ForgeSelect>

// Select with default value
<ForgeSelect 
  name="language"
  value={selectedLanguage}
  onChange={handleLanguageChange}
>
  <option value="">Select language</option>
  <option value="en">English</option>
  <option value="es">Spanish</option>
  <option value="fr">French</option>
  <option value="de">German</option>
</ForgeSelect>

// Required select with validation
<ForgeSelect 
  name="category"
  required
  error={hasError}
  errorMessage="Please select a category"
>
  <option value="">Choose category</option>
  <option value="electronics">Electronics</option>
  <option value="clothing">Clothing</option>
  <option value="books">Books</option>
  <option value="home">Home & Garden</option>
</ForgeSelect>
// Searchable select for large datasets
<ForgeSelect 
  name="city"
  placeholder="Search for a city"
  searchable
  searchPlaceholder="Type to search cities..."
  onChange={handleCityChange}
>
  <option value="new-york">New York</option>
  <option value="los-angeles">Los Angeles</option>
  <option value="chicago">Chicago</option>
  <option value="houston">Houston</option>
  <option value="phoenix">Phoenix</option>
  <option value="philadelphia">Philadelphia</option>
  <option value="san-antonio">San Antonio</option>
  <option value="san-diego">San Diego</option>
  <option value="dallas">Dallas</option>
  <option value="san-jose">San Jose</option>
</ForgeSelect>

Advanced Usage

Select with Icons and Descriptions

function PaymentMethodSelect() {
  const [paymentMethod, setPaymentMethod] = useState('');
  
  return (
    <ForgeSelect
      name="paymentMethod"
      value={paymentMethod}
      onChange={setPaymentMethod}
      placeholder="Select payment method"
      size="lg"
    >
      <option value="card" data-icon="credit-card">
        Credit Card
      </option>
      <option value="paypal" data-icon="paypal">
        PayPal
      </option>
      <option value="apple-pay" data-icon="apple">
        Apple Pay
      </option>
      <option value="google-pay" data-icon="google">
        Google Pay
      </option>
      <option value="bank" data-icon="bank">
        Bank Transfer
      </option>
    </ForgeSelect>
  );
}

Dynamic Options with API Data

function CountrySelect() {
  const [countries, setCountries] = useState([]);
  const [loading, setLoading] = useState(true);
  const [selectedCountry, setSelectedCountry] = useState('');
  
  useEffect(() => {
    fetchCountries()
      .then(data => {
        setCountries(data);
        setLoading(false);
      })
      .catch(error => {
        console.error('Failed to load countries:', error);
        setLoading(false);
      });
  }, []);
  
  if (loading) {
    return (
      <ForgeSelect disabled placeholder="Loading countries...">
        <option value="">Loading...</option>
      </ForgeSelect>
    );
  }
  
  return (
    <ForgeSelect
      name="country"
      value={selectedCountry}
      onChange={setSelectedCountry}
      placeholder="Select your country"
      searchable
      searchPlaceholder="Search countries..."
    >
      <option value="">Select country</option>
      {countries.map(country => (
        <option key={country.code} value={country.code}>
          {country.name}
        </option>
      ))}
    </ForgeSelect>
  );
}

Form Integration with Validation

function ProfileForm() {
  const [profile, setProfile] = useState({
    title: '',
    department: '',
    location: '',
    timezone: ''
  });
  
  const [errors, setErrors] = useState({});
  
  const handleSelectChange = (name, value) => {
    setProfile(prev => ({ ...prev, [name]: value }));
    
    // Clear error when user makes selection
    if (errors[name]) {
      setErrors(prev => ({ ...prev, [name]: '' }));
    }
  };
  
  const validate = () => {
    const newErrors = {};
    if (!profile.title) newErrors.title = 'Title is required';
    if (!profile.department) newErrors.department = 'Department is required';
    if (!profile.location) newErrors.location = 'Location is required';
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (validate()) {
      console.log('Profile:', profile);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <ForgeSelect
        name="title"
        value={profile.title}
        onChange={(value) => handleSelectChange('title', value)}
        placeholder="Select title"
        required
        error={!!errors.title}
        errorMessage={errors.title}
      >
        <option value="">Choose title</option>
        <option value="mr">Mr.</option>
        <option value="ms">Ms.</option>
        <option value="mrs">Mrs.</option>
        <option value="dr">Dr.</option>
        <option value="prof">Prof.</option>
      </ForgeSelect>
      
      <ForgeSelect
        name="department"
        value={profile.department}
        onChange={(value) => handleSelectChange('department', value)}
        placeholder="Select department"
        required
        error={!!errors.department}
        errorMessage={errors.department}
        searchable
      >
        <option value="">Choose department</option>
        <option value="engineering">Engineering</option>
        <option value="marketing">Marketing</option>
        <option value="sales">Sales</option>
        <option value="support">Customer Support</option>
        <option value="hr">Human Resources</option>
        <option value="finance">Finance</option>
        <option value="operations">Operations</option>
      </ForgeSelect>
      
      <ForgeSelect
        name="timezone"
        value={profile.timezone}
        onChange={(value) => handleSelectChange('timezone', value)}
        placeholder="Select timezone"
        searchable
        searchPlaceholder="Search timezones..."
      >
        <option value="">Choose timezone</option>
        <option value="EST">Eastern Time (EST)</option>
        <option value="CST">Central Time (CST)</option>
        <option value="MST">Mountain Time (MST)</option>
        <option value="PST">Pacific Time (PST)</option>
        <option value="GMT">Greenwich Mean Time (GMT)</option>
        <option value="CET">Central European Time (CET)</option>
      </ForgeSelect>
      
      <ForgeButton type="submit">Save Profile</ForgeButton>
    </form>
  );
}

Properties

PropertyTypeDefaultDescription
namestringundefinedForm field name
valuestringundefinedSelected value
placeholderstringundefinedPlaceholder text
disabledbooleanfalseDisable select interactions
requiredbooleanfalseMark as required field
size'sm' | 'md' | 'lg''md'Select size
errorbooleanfalseShow error state
successbooleanfalseShow success state
warningbooleanfalseShow warning state
errorMessagestringundefinedError message text
helpTextstringundefinedHelp text description
searchablebooleanfalseEnable search functionality
searchPlaceholderstring'Search...'Search input placeholder
clearablebooleanfalseShow clear button
loadingbooleanfalseShow loading state
multiplebooleanfalseEnable multiple selection
maxHeightstring'300px'Maximum dropdown height

Events

EventDetailDescription
change{ value: string, name: string, option: HTMLOptionElement }Fired when selection changes
search{ query: string }Fired when user types in search
openvoidFired when dropdown opens
closevoidFired when dropdown closes
focusFocusEventFired when select receives focus
blurFocusEventFired when select loses focus

Methods

MethodParametersReturnsDescription
open()-voidOpen the dropdown
close()-voidClose the dropdown
focus()-voidFocus the select
clear()-voidClear the selection
selectOption(value)stringvoidProgrammatically select option

SSR Fallback Behavior

In SSR environments, ForgeSelect renders a semantic HTML select that provides full functionality even without JavaScript:

Server-Side Rendering

<!-- Basic select rendered during SSR -->
<div class="forge-select forge-select--md">
  <select name="country" class="forge-select__native" required>
    <option value="" disabled selected>Select country</option>
    <option value="us">United States</option>
    <option value="ca">Canada</option>
    <option value="uk">United Kingdom</option>
    <option value="de">Germany</option>
    <option value="fr">France</option>
  </select>
  <div class="forge-select__icon" aria-hidden="true">
    <svg><!-- Chevron down icon --></svg>
  </div>
</div>

<!-- Select with error state -->
<div class="forge-select forge-select--error">
  <select 
    name="category" 
    class="forge-select__native" 
    required
    aria-describedby="category-error"
    aria-invalid="true"
  >
    <option value="">Choose category</option>
    <option value="electronics">Electronics</option>
    <option value="clothing">Clothing</option>
  </select>
  <div class="forge-select__icon" aria-hidden="true">
    <svg><!-- Chevron down icon --></svg>
  </div>
  <div id="category-error" class="forge-select__error">
    Please select a category
  </div>
</div>

Client-Side Hydration

<!-- After hydration to web component -->
<forge-select name="country" placeholder="Select country" required>
  <option value="us">United States</option>
  <option value="ca">Canada</option>
  <option value="uk">United Kingdom</option>
</forge-select>

<forge-select 
  name="category" 
  error 
  error-message="Please select a category"
  required>
  <option value="">Choose category</option>
  <option value="electronics">Electronics</option>
  <option value="clothing">Clothing</option>
</forge-select>

Accessibility

WCAG 2.1 AA Compliance

  • Keyboard Navigation: Full Tab, Arrow key, Enter, Escape support
  • Focus Management: Proper focus indicators and focus trapping in dropdown
  • Screen Reader Support: Native select semantics with ARIA enhancements
  • Form Integration: Native form submission and validation
  • Error Handling: Accessible error message association
  • Search Functionality: Accessible search with live results announcement

ARIA Support

<ForgeSelect
  name="priority"
  placeholder="Select priority level"
  required
  aria-describedby="priority-help priority-error"
  error={hasError}
  errorMessage="Priority level is required"
>
  <option value="low">Low priority</option>
  <option value="medium">Medium priority</option>
  <option value="high">High priority</option>
  <option value="urgent">Urgent priority</option>
</ForgeSelect>

<div id="priority-help" className="help-text">
  Choose the appropriate priority level for this task
</div>

Keyboard Support

KeyAction
TabFocus select or move to next element
Shift + TabFocus previous element
Space / EnterOpen/close dropdown
Arrow Up/DownNavigate options
HomeGo to first option
EndGo to last option
EscapeClose dropdown
A-ZJump to option starting with letter

Framework Integration

Next.js App Router (SSR)

// app/components/LocationSelector.tsx - Automatic SSR
import { ForgeSelect } from '@nexcraft/forge/integrations/react';

export default function LocationSelector({ countries, states, cities }) {
  return (
    <div className="location-selector">
      <ForgeSelect name="country" placeholder="Select country" required>
        <option value="">Choose country</option>
        {countries.map(country => (
          <option key={country.code} value={country.code}>
            {country.name}
          </option>
        ))}
      </ForgeSelect>
      
      <ForgeSelect name="state" placeholder="Select state" required>
        <option value="">Choose state</option>
        {states.map(state => (
          <option key={state.code} value={state.code}>
            {state.name}
          </option>
        ))}
      </ForgeSelect>
      
      <ForgeSelect name="city" placeholder="Select city" searchable>
        <option value="">Choose city</option>
        {cities.map(city => (
          <option key={city.id} value={city.id}>
            {city.name}
          </option>
        ))}
      </ForgeSelect>
    </div>
  );
}

Vite/CRA (Client-only)

// Same component, automatically renders as web component
import { ForgeSelect } from '@nexcraft/forge/integrations/react';

function FilterControls({ onFilterChange }) {
  const [filters, setFilters] = useState({
    category: '',
    priceRange: '',
    brand: '',
    rating: ''
  });
  
  const handleFilterChange = (name, value) => {
    const newFilters = { ...filters, [name]: value };
    setFilters(newFilters);
    onFilterChange(newFilters);
  };
  
  return (
    <div className="filter-controls">
      <ForgeSelect
        name="category"
        value={filters.category}
        onChange={(value) => handleFilterChange('category', value)}
        placeholder="All categories"
        clearable
      >
        <option value="electronics">Electronics</option>
        <option value="clothing">Clothing</option>
        <option value="books">Books</option>
        <option value="home">Home & Garden</option>
      </ForgeSelect>
      
      <ForgeSelect
        name="priceRange"
        value={filters.priceRange}
        onChange={(value) => handleFilterChange('priceRange', value)}
        placeholder="Any price"
        clearable
      >
        <option value="0-25">\$0 - \$25</option>
        <option value="25-50">\$25 - \$50</option>
        <option value="50-100">\$50 - \$100</option>
        <option value="100+">\$100+</option>
      </ForgeSelect>
      
      <ForgeSelect
        name="rating"
        value={filters.rating}
        onChange={(value) => handleFilterChange('rating', value)}
        placeholder="Any rating"
        clearable
      >
        <option value="4+">4+ Stars</option>
        <option value="3+">3+ Stars</option>
        <option value="2+">2+ Stars</option>
        <option value="1+">1+ Stars</option>
      </ForgeSelect>
    </div>
  );
}

Examples

Multi-Step Form Select Chain

function AddressForm() {
  const [address, setAddress] = useState({
    country: '',
    state: '',
    city: ''
  });
  
  const [states, setStates] = useState([]);
  const [cities, setCities] = useState([]);
  
  useEffect(() => {
    if (address.country) {
      fetchStatesByCountry(address.country).then(setStates);
      setAddress(prev => ({ ...prev, state: '', city: '' }));
      setCities([]);
    }
  }, [address.country]);
  
  useEffect(() => {
    if (address.state) {
      fetchCitiesByState(address.state).then(setCities);
      setAddress(prev => ({ ...prev, city: '' }));
    }
  }, [address.state]);
  
  return (
    <form>
      <ForgeSelect
        name="country"
        value={address.country}
        onChange={(value) => setAddress(prev => ({ ...prev, country: value }))}
        placeholder="Select country"
        required
      >
        <option value="">Choose country</option>
        <option value="US">United States</option>
        <option value="CA">Canada</option>
        <option value="UK">United Kingdom</option>
      </ForgeSelect>
      
      <ForgeSelect
        name="state"
        value={address.state}
        onChange={(value) => setAddress(prev => ({ ...prev, state: value }))}
        placeholder="Select state"
        disabled={!address.country}
        required
      >
        <option value="">Choose state</option>
        {states.map(state => (
          <option key={state.code} value={state.code}>
            {state.name}
          </option>
        ))}
      </ForgeSelect>
      
      <ForgeSelect
        name="city"
        value={address.city}
        onChange={(value) => setAddress(prev => ({ ...prev, city: value }))}
        placeholder="Select city"
        disabled={!address.state}
        searchable
        searchPlaceholder="Search cities..."
      >
        <option value="">Choose city</option>
        {cities.map(city => (
          <option key={city.id} value={city.id}>
            {city.name}
          </option>
        ))}
      </ForgeSelect>
    </form>
  );
}

CSS Custom Properties

forge-select {
  /* Base styles */
  --select-border-radius: 6px;
  --select-border-width: 1px;
  --select-transition: all 0.2s ease;
  --select-font-family: inherit;
  --select-font-weight: 400;
  --select-min-height: 40px;
  
  /* Size variants */
  --select-sm-height: 32px;
  --select-sm-padding: 0 32px 0 8px;
  --select-sm-font-size: 13px;
  
  --select-md-height: 40px;
  --select-md-padding: 0 40px 0 12px;
  --select-md-font-size: 14px;
  
  --select-lg-height: 48px;
  --select-lg-padding: 0 48px 0 16px;
  --select-lg-font-size: 16px;
  
  /* Colors */
  --select-bg: white;
  --select-border: var(--forge-color-neutral-300);
  --select-text: var(--forge-color-neutral-900);
  --select-placeholder: var(--forge-color-neutral-500);
  
  /* Focus state */
  --select-focus-border: var(--forge-color-primary-500);
  --select-focus-ring: 0 0 0 3px rgba(59, 130, 246, 0.1);
  --select-focus-bg: white;
  
  /* Hover state */
  --select-hover-border: var(--forge-color-neutral-400);
  --select-hover-bg: var(--forge-color-neutral-50);
  
  /* Error state */
  --select-error-border: var(--forge-color-error-500);
  --select-error-bg: var(--forge-color-error-50);
  --select-error-text: var(--forge-color-error-700);
  
  /* Success state */
  --select-success-border: var(--forge-color-success-500);
  --select-success-bg: var(--forge-color-success-50);
  
  /* Disabled state */
  --select-disabled-bg: var(--forge-color-neutral-100);
  --select-disabled-border: var(--forge-color-neutral-200);
  --select-disabled-text: var(--forge-color-neutral-400);
  --select-disabled-cursor: not-allowed;
  
  /* Dropdown */
  --select-dropdown-bg: white;
  --select-dropdown-border: var(--forge-color-neutral-200);
  --select-dropdown-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  --select-dropdown-border-radius: 6px;
  --select-dropdown-max-height: 300px;
  
  /* Options */
  --select-option-padding: 8px 12px;
  --select-option-hover-bg: var(--forge-color-neutral-100);
  --select-option-selected-bg: var(--forge-color-primary-50);
  --select-option-selected-color: var(--forge-color-primary-700);
  
  /* Icon */
  --select-icon-color: var(--forge-color-neutral-400);
  --select-icon-size: 16px;
  --select-icon-rotation: 180deg; /* When open */
  
  /* Search */
  --select-search-border: var(--forge-color-neutral-200);
  --select-search-bg: white;
  --select-search-padding: 8px 12px;
  
  /* Animation */
  --select-animation-duration: 200ms;
  --select-animation-timing: ease-out;
}

AI Integration

State Explanation

const select = document.querySelector('forge-select');
console.log(select.explainState());
// "Select dropdown with 5 options, 'canada' is selected, searchable, no validation errors"

Possible Actions

const actions = select.getPossibleActions();
// [
//   { name: 'open', available: true, description: 'Open the dropdown' },
//   { name: 'clear', available: true, description: 'Clear the selection' },
//   { name: 'search', available: true, description: 'Search through options' }
// ]

Performance

  • Lightweight: <6KB gzipped including search functionality
  • SSR Optimized: Native HTML select with progressive enhancement
  • Virtual Scrolling: Handles large option lists efficiently
  • Debounced Search: Optimized search performance
  • Memory Efficient: Automatic cleanup and event management

Browser Support

  • Modern Browsers: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
  • SSR Fallback: Works in any browser with HTML form support
  • Progressive Enhancement: Full functionality without JavaScript in SSR
  • Mobile Support: Touch-friendly with native mobile select behavior