Skip to content

Input

A flexible text input component with labels, hints, validation states, icons, and full Pulse Framework reactivity support.

Import

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

Basic Usage

Code Éditable
Résultat

Input Types

The Input component supports various HTML5 input types.

Code Éditable
Résultat

With Label

Code Éditable
Résultat

With Hint

Code Éditable
Résultat

Validation States

Code Éditable
Résultat

With Icon

Code Éditable
Résultat

Sizes

Three size options are available: sm, md, and lg.

Code Éditable
Résultat

Reactive Input with Signals

Bind input values to Pulse signals for full reactivity.

tsx
const email = Pulse.signal('');

const reactiveInput = (
  <Input 
    label="Email"
    type="email"
    value={email}
    onChange={(val) => email(val)}
    placeholder="you@example.com"
  />
);

// Use the value elsewhere
Pulse.effect(() => {
  console.log('Email changed:', email());
});

Required Fields

Code Éditable
Résultat

Disabled State

Code Éditable
Résultat

Readonly State

Code Éditable
Résultat

Reactive Validation

Validate input in real-time with computed values.

tsx
const email = Pulse.signal('');

const isValidEmail = Pulse.computed(() => {
  const value = email();
  return value.includes('@') && value.includes('.');
});

const errorMessage = Pulse.computed(() => {
  const value = email();
  if (value.length === 0) return '';
  return isValidEmail() ? '' : 'Please enter a valid email address';
});

const validatedInput = (
  <Input 
    label="Email Address"
    type="email"
    value={email}
    onChange={(val) => email(val)}
    error={errorMessage}
    placeholder="you@example.com"
  />
);

Complete Example - Login Form

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

const LoginForm = () => {
  const email = Pulse.signal('');
  const password = Pulse.signal('');
  const isLoading = Pulse.signal(false);
  
  const isValidEmail = Pulse.computed(() => {
    const val = email();
    return val.includes('@') && val.includes('.');
  });
  
  const isValidPassword = Pulse.computed(() => {
    return password().length >= 8;
  });
  
  const isFormValid = Pulse.computed(() => {
    return isValidEmail() && isValidPassword();
  });
  
  const emailError = Pulse.computed(() => {
    const val = email();
    if (val.length === 0) return '';
    return isValidEmail() ? '' : 'Invalid email format';
  });
  
  const passwordError = Pulse.computed(() => {
    const val = password();
    if (val.length === 0) return '';
    return isValidPassword() ? '' : 'Password must be at least 8 characters';
  });

  const handleSubmit = async (e: Event) => {
    e.preventDefault();
    
    if (!isFormValid()) return;
    
    isLoading(true);
    
    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 2000));
      console.log('Login successful!', { email: email(), password: password() });
    } catch (error) {
      console.error('Login failed:', error);
    } finally {
      isLoading(false);
    }
  };

  return (
    <form onsubmit={handleSubmit} class="space-y-4 max-w-md">
      <Input
        type="email"
        label="Email Address"
        placeholder="you@example.com"
        icon="✉️"
        value={email}
        onChange={(val) => email(val)}
        error={emailError}
        required={true}
      />
      
      <Input
        type="password"
        label="Password"
        placeholder="••••••••"
        icon="🔒"
        value={password}
        onChange={(val) => password(val)}
        error={passwordError}
        hint="Must be at least 8 characters"
        required={true}
      />
      
      <Button
        type="submit"
        color="primary"
        fullWidth={true}
        loading={isLoading}
        disabled={Pulse.computed(() => !isFormValid())}
      >
        Sign In
      </Button>
    </form>
  ) as Pulse.JSX.Element;
};

Props

PropTypeDefaultDescription
type"text" | "email" | "password" | "number" | "tel" | "url" | "search""text"Input type
valuestring | Signal<string>-Input value (reactive or static)
placeholderstring""Placeholder text
labelstring-Label text
hintstring-Hint text below input
errorstring | Signal<string>-Error message
successstring | Signal<string>-Success message
size"sm" | "md" | "lg""md"Input size
iconstring-Icon to display
iconPosition"left" | "right""left"Icon position
disabledbooleanfalseDisable input
readonlybooleanfalseMake readonly
requiredbooleanfalseMark as required
onChange(value: string) => void-Change callback
classNamestring-Additional CSS classes

Accessibility

The Input component follows accessibility best practices:

  • ✅ Proper <label> association with for attribute
  • aria-describedby for hint and error messages
  • aria-invalid when in error state
  • required attribute for required fields
  • ✅ Keyboard navigation support
  • ✅ Screen reader friendly

ARIA Attributes

tsx
const accessibleInput = (
  <Input
    label="Email"
    type="email"
    aria-label="Email address"
    aria-describedby="email-hint"
  />
);

Best Practices

✅ Do

  • Use appropriate input types for better UX and validation
  • Provide clear labels for all inputs
  • Use hints to guide users
  • Show validation errors inline
  • Use signals for reactive forms
tsx
// Good: Clear label, type, validation
const goodInput = (
  <Input 
    type="email"
    label="Email Address"
    hint="We'll never share your email"
    required={true}
  />
);

❌ Don't

  • Don't omit labels (except search inputs)
  • Don't use vague placeholder text
  • Don't validate before user interaction
  • Don't forget to handle onChange
tsx
// Bad: No label, vague placeholder
const badInput = (
  <Input placeholder="Enter something" />
);

// Better: Clear label and placeholder
const betterInput = (
  <Input 
    label="Full Name"
    placeholder="John Doe"
  />
);

Common Patterns

Pattern 1: Search Input

tsx
const searchQuery = Pulse.signal('');

const searchInput = (
  <Input
    type="search"
    icon="🔍"
    iconPosition="left"
    placeholder="Search articles..."
    value={searchQuery}
    onChange={(val) => searchQuery(val)}
  />
);

Pattern 2: Password Strength Indicator

tsx
const password = Pulse.signal('');

const passwordStrength = Pulse.computed(() => {
  const val = password();
  if (val.length < 6) return 'weak';
  if (val.length < 10) return 'medium';
  return 'strong';
});

const strengthColor = Pulse.computed(() => {
  const strength = passwordStrength();
  return strength === 'weak' ? 'danger' : 
         strength === 'medium' ? 'warning' : 'success';
});

const passwordInput = (
  <div>
    <Input
      type="password"
      label="Password"
      value={password}
      onChange={(val) => password(val)}
      hint={`Strength: ${passwordStrength()}`}
    />
  </div>
);
tsx
const searchQuery = Pulse.signal('');
let debounceTimer: number;

const debouncedSearch = (value: string) => {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => {
    console.log('Searching for:', value);
    // Perform search
  }, 300);
};

const debouncedInput = (
  <Input
    type="search"
    placeholder="Search..."
    value={searchQuery}
    onChange={(val) => {
      searchQuery(val);
      debouncedSearch(val);
    }}
  />
);

Styling & Theming

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

Custom Styling

tsx
const customInput = (
  <Input 
    className="border-2 border-blue-500 focus:ring-4"
    placeholder="Custom styled input"
  />
);

TypeScript

Full TypeScript support with complete type definitions:

tsx
import type { InputProps } from '@odyssee/components';

const props: InputProps = {
  type: 'email',
  label: 'Email',
  required: true,
  onChange: (value: string) => {
    console.log('Value changed:', value);
  }
};

const input = <Input {...props} />;

Version: 1.0.0
Last Updated: January 2025

Released under the MIT License.