Zoeken...  ⌘K GitHub

InputField UI Elements

Formulier input met label, helper tekst, error state, iconen en password toggle. Textarea variant inbegrepen.

/input-field
src/components/ui/InputField.astro
---
/**
 * InputField
 * Styled form input: text, email, password, textarea.
 * Label, helper text, error state, disabled, leading/trailing icon.
 */
interface Props {
  id?: string;
  name?: string;
  label?: string;
  type?: 'text' | 'email' | 'password' | 'tel' | 'url' | 'search' | 'number' | 'textarea';
  placeholder?: string;
  value?: string;
  helper?: string;
  error?: string;
  required?: boolean;
  disabled?: boolean;
  rows?: number;
  leadingIcon?: string;
  trailingIcon?: string;
  size?: 'sm' | 'md' | 'lg';
}

const {
  id,
  name,
  label,
  type = 'text',
  placeholder,
  value,
  helper,
  error,
  required = false,
  disabled = false,
  rows = 4,
  leadingIcon,
  trailingIcon,
  size = 'md',
} = Astro.props;

const inputId = id ?? `input-${Math.random().toString(36).slice(2, 7)}`;
const hasError = !!error;
---

<div class:list={['if', `if--${size}`, { 'if--error': hasError, 'if--disabled': disabled }]} data-component="input-field">
  {label && (
    <label class="if__label" for={inputId}>
      {label}
      {required && <span class="if__required" aria-hidden="true">*</span>}
    </label>
  )}

  <div class="if__wrap">
    {leadingIcon && (
      <span class="if__icon if__icon--lead" aria-hidden="true" set:html={leadingIcon} />
    )}

    {type === 'textarea' ? (
      <textarea
        id={inputId}
        name={name}
        class="if__input if__textarea"
        placeholder={placeholder}
        rows={rows}
        required={required}
        disabled={disabled}
        aria-invalid={hasError ? 'true' : undefined}
        aria-describedby={error ? `${inputId}-error` : helper ? `${inputId}-helper` : undefined}
      >{value}</textarea>
    ) : (
      <input
        id={inputId}
        name={name}
        type={type}
        class="if__input"
        placeholder={placeholder}
        value={value}
        required={required}
        disabled={disabled}
        aria-invalid={hasError ? 'true' : undefined}
        aria-describedby={error ? `${inputId}-error` : helper ? `${inputId}-helper` : undefined}
      />
    )}

    {trailingIcon && (
      <span class="if__icon if__icon--trail" aria-hidden="true" set:html={trailingIcon} />
    )}

    {type === 'password' && (
      <button type="button" class="if__toggle-pw" aria-label="Toon/verberg wachtwoord">
        <svg class="if__pw-show" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
          <circle cx="12" cy="12" r="3"/>
        </svg>
      </button>
    )}
  </div>

  {error && <p class="if__message if__message--error" id={`${inputId}-error`} role="alert">{error}</p>}
  {!error && helper && <p class="if__message" id={`${inputId}-helper`}>{helper}</p>}
</div>

{type === 'password' && (
  <script>
    document.querySelectorAll('[data-component="input-field"]').forEach(field => {
      const btn = field.querySelector<HTMLButtonElement>('.if__toggle-pw');
      const input = field.querySelector<HTMLInputElement>('.if__input');
      if (btn && input) {
        btn.addEventListener('click', () => {
          input.type = input.type === 'password' ? 'text' : 'password';
        });
      }
    });
  </script>
)}

<style>
  .if { display: flex; flex-direction: column; gap: 0.375rem; }

  .if__label {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--color-primary, #0a0a0a);
    display: flex;
    align-items: center;
    gap: 0.25rem;
  }

  .if--sm .if__label { font-size: 0.8125rem; }
  .if--lg .if__label { font-size: 0.9375rem; }

  .if__required { color: #ef4444; }

  .if__wrap {
    position: relative;
    display: flex;
    align-items: center;
  }

  .if__input {
    width: 100%;
    background: var(--color-bg, #fff);
    border: 1.5px solid rgba(0,0,0,0.14);
    border-radius: var(--radius, 0.5rem);
    font-size: 0.9375rem;
    font-family: inherit;
    color: var(--color-primary, #0a0a0a);
    padding: 0.6875rem 0.875rem;
    outline: none;
    transition: border-color 0.15s, box-shadow 0.15s;
    appearance: none;
    -webkit-appearance: none;
  }

  .if--sm .if__input { font-size: 0.8125rem; padding: 0.5rem 0.75rem; }
  .if--lg .if__input { font-size: 1rem; padding: 0.875rem 1rem; }

  .if__input::placeholder { color: rgba(0,0,0,0.3); }

  .if__input:focus {
    border-color: var(--color-accent, #6366f1);
    box-shadow: 0 0 0 3px rgba(99,102,241,0.12);
  }

  .if--error .if__input {
    border-color: #ef4444;
  }

  .if--error .if__input:focus {
    box-shadow: 0 0 0 3px rgba(239,68,68,0.12);
  }

  .if--disabled .if__input {
    background: rgba(0,0,0,0.04);
    color: rgba(0,0,0,0.4);
    cursor: not-allowed;
  }

  /* Icons */
  .if__icon {
    position: absolute;
    display: flex;
    align-items: center;
    color: rgba(0,0,0,0.4);
    pointer-events: none;
  }

  .if__icon--lead { left: 0.75rem; }
  .if__icon--trail { right: 0.75rem; }

  .if__wrap:has(.if__icon--lead) .if__input { padding-left: 2.5rem; }
  .if__wrap:has(.if__icon--trail) .if__input { padding-right: 2.5rem; }

  /* Textarea */
  .if__textarea {
    resize: vertical;
    align-self: stretch;
    min-height: 100px;
  }

  /* Password toggle */
  .if__toggle-pw {
    position: absolute;
    right: 0.75rem;
    background: none;
    border: none;
    cursor: pointer;
    color: rgba(0,0,0,0.4);
    padding: 0.25rem;
    display: flex;
    align-items: center;
    transition: color 0.15s;
  }

  .if__toggle-pw:hover { color: var(--color-primary, #0a0a0a); }

  /* Messages */
  .if__message {
    font-size: 0.8125rem;
    color: var(--color-muted, #6b7280);
    line-height: 1.4;
  }

  .if__message--error { color: #ef4444; }
</style>

Props

Prop Type Default Beschrijving
label string Veld label
type 'text' | 'email' | 'password' | 'tel' | 'url' | 'textarea' 'text' Input type
placeholder string Placeholder tekst
value string Standaard waarde
helper string Hulptekst onder het veld
error string Foutmelding (toont rode staat)
required boolean false Verplicht veld
disabled boolean false Uitgeschakeld
size 'sm' | 'md' | 'lg' 'md' Grootte
leadingIcon string SVG HTML voor icoon links
trailingIcon string SVG HTML voor icoon rechts
rows number 4 Aantal rijen (type=textarea)

* = verplicht