Zum Inhalt springen
UI Kit
Accent

Input Feld für React

Textfeld mit Label, Hinweis, Fehler als Text, Icons, Präfix, Suffix und Anhängseln.

Eine von 9 Components in Forms und Inputs

4 Variants

Das Textfeld, richtig gemacht

Das Input ist das Feld, mit dem jedes Formular beginnt. Dieses verbindet Label, Hinweis und Fehler mit den passenden Attributen, Screenreader lesen also alle drei vor, und es zeigt Fehler als Text neben dem Feld statt nur als roten Rahmen.

„Zustände“ zeigt Standard, Fokus, ausgefüllt, deaktiviert, schreibgeschützt und Fehler nebeneinander. „Icons und Anhängsel“ ergänzt ein Such- oder Mail-Icon, ein Präfix wie https oder eine Währung, ein Suffix wie eine Einheit und angehängte Buttons. „Größen“ entspricht den drei Button-Größen, Input und Button in einer Reihe fluchten also. „Passwort“ hat einen Knopf zum Ein- und Ausblenden und das richtige autocomplete für neue und aktuelle Passwörter.

Setz type und autocomplete für jedes Feld, Handys zeigen dann die passende Tastatur und füllen Bekanntes aus. Lass das Label sichtbar, ein Platzhalter ist ein Hinweis und verschwindet beim Tippen. Sag im Fehler, was zu tun ist, Datum wie 24.12.2026 eingeben statt Ungültige Eingabe.

Variants

Zustände

Label, Hinweis, optional, Pflicht, Fehler, Erfolg und gesperrt. Der Fokusring blendet weich ein, ein Fehler schüttelt einmal, eine gültige Nummer zeichnet einen Haken. Fehler stehen als Text da, nie nur als Farbe.

Icons und Anhängsel

Icon vorne, Präfix und Suffix im Feld, angehängte Segmente und ein Knopf im Feld.

Größen

Klein, mittel und groß, jeweils so hoch wie der Knopf derselben Größe. Das letzte hat ein schwebendes Label, 56 px hoch, es steigt beim Tippen auf.

Passwort

Zeigen und verbergen mit beschriftetem Knopf, Link in der Labelzeile und Fehler.

Details

Was Eingabefeld mitbringt, aus Registry und Quellcode gelesen.

Bereich
Elements
Variants
4
Version
1.0.1 · seit 1.0.0
Datei
components/ui/input.tsx
Bringt mit
utilskiticon
npm-Pakete
Keine
Sprachen
Englisch und Deutsch, 3 eigene Texte
Studio
Im Code anpassen

Props

Die Schnittstelle InputProps aus components/ui/input.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch. Nimmt außerdem alle Props von FieldOwnProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix'> an.

  • sizeoptional
    ControlSize

    smmdlg

  • iconoptional
    IconName

    Icon at the start

  • prefixoptional
    React.ReactNode

    Quiet text inside the box, before the value, e.g. "https://"

  • suffixoptional
    React.ReactNode

    Quiet text inside the box, after the value, e.g. "€" or "kg"

  • startAddonoptional
    React.ReactNode

    A boxed segment attached at the start, e.g. a protocol or a country code

  • endAddonoptional
    React.ReactNode

    A boxed segment attached at the end, e.g. a domain

  • trailingoptional
    React.ReactNode

    Any element at the end inside the box, e.g. a button, a key hint or a spinner

  • invalidoptional
    boolean

    Marks the control invalid without an error text (use error for text). The box shakes once when it turns invalid.

  • successoptional
    boolean

    Marks the value as accepted: a green edge and a check that draws itself

  • floatingLabeloptional
    boolean

    The label sits inside the box like a placeholder and floats up when the field has focus or a value. The box is 56 px tall. Off by default.

  • refoptional
    React.Ref<HTMLInputElement>
  • inputClassNameoptional
    string

Eingebaute Texte

Diese Texte schreibt Eingabefeld selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.

Alle 3 Texte anzeigen
optionalEnglisch OptionalDeutsch Optional
showEnglisch Show passwordDeutsch Passwort zeigen
hideEnglisch Hide passwordDeutsch Passwort verbergen

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Setzt aria-describedby, aria-invalid, aria-label, aria-live und aria-pressed, damit Screenreader Zweck und Zustand ansagen.
  • Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
  • Jedes Feld hat ein Label, sichtbar oder für Screenreader.
  • Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
  • Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.

Bewegung

  • Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 2 Stellen im Code schalten Bewegung dann ab.
  • Ohne Animationen, sie steht sofort ruhig da.

Quellcode

Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.

Datei anzeigen, 442 Zeilen
components/ui/input.tsx
'use client';

import { createContext, useContext, useEffect, useId, useRef, useState } from 'react';
import { cn, prefersReducedMotion, useTexts } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';

/*
 * Text input and the field system every form control shares: a label,
 * a hint, an error as text, and the wiring between them (id, aria-describedby,
 * aria-invalid). Wrap any control in <Field>, or pass label/hint/error
 * straight to the control.
 */

const TEXTS = {
  en: { optional: 'Optional', show: 'Show password', hide: 'Hide password' },
  de: { optional: 'Optional', show: 'Passwort zeigen', hide: 'Passwort verbergen' },
};

export type ControlSize = 'sm' | 'md' | 'lg';

/** Heights match the button sizes, so inputs and buttons line up in a row (small grows to 44 px on phones, like the small button). 16 px text on phones keeps iOS from zooming. */
export const CONTROL_HEIGHT: Record<ControlSize, string> = {
  sm: 'h-11 rounded-md text-base sm:h-9 sm:text-[13px]',
  md: 'h-11 rounded-lg text-base sm:text-sm',
  lg: 'h-12 rounded-lg text-base sm:text-[15px]',
};
export const CONTROL_PAD: Record<ControlSize, string> = { sm: 'px-3', md: 'px-3.5', lg: 'px-4' };
const PAD_START: Record<ControlSize, string> = { sm: 'ps-3', md: 'ps-3.5', lg: 'ps-4' };
const PAD_END: Record<ControlSize, string> = { sm: 'pe-3', md: 'pe-3.5', lg: 'pe-4' };
const MARGIN_START: Record<ControlSize, string> = { sm: 'ms-3', md: 'ms-3.5', lg: 'ms-4' };
const ICON_SIZE: Record<ControlSize, number> = { sm: 15, md: 17, lg: 18 };

/** The box of every control: border, surface, hover, focus ring, invalid and disabled */
export const controlBox = cn(
  // The focus ring grows out of the border and fades in, it does not snap on
  'border border-input bg-background text-foreground shadow-[var(--shadow-sm)] transition-[border-color,box-shadow,background-color,scale] duration-300 ease-[var(--ease-kit)]',
  'hover:border-border-strong',
  'focus-within:border-[color-mix(in_oklab,var(--primary)_60%,var(--border-strong))] focus-within:shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_16%,transparent),var(--shadow-sm)]',
  'data-invalid:border-[color-mix(in_oklab,var(--destructive)_75%,var(--border))] data-invalid:focus-within:shadow-[0_0_0_3px_color-mix(in_oklab,var(--destructive)_16%,transparent),var(--shadow-sm)]',
  'data-disabled:cursor-not-allowed data-disabled:bg-muted data-disabled:text-muted-foreground data-disabled:shadow-none data-disabled:hover:border-input',
);

/** The same box for a single focusable element (select trigger, buttons that look like fields) */
export const controlBoxFocus = cn(
  controlBox,
  'outline-none focus-visible:border-[color-mix(in_oklab,var(--primary)_60%,var(--border-strong))] focus-visible:shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_16%,transparent),var(--shadow-sm)]',
  'aria-invalid:border-[color-mix(in_oklab,var(--destructive)_75%,var(--border))]',
  'disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground disabled:shadow-none',
);

/* ------------------------------------------------------------------ */
/* Motion shared by the form controls                                  */
/* ------------------------------------------------------------------ */

const EASE = 'cubic-bezier(0.2,0.7,0.2,1)';
const SHAKE: Keyframe[] = [
  { transform: 'translateX(0)' },
  { transform: 'translateX(-6px)' },
  { transform: 'translateX(5px)' },
  { transform: 'translateX(-3px)' },
  { transform: 'translateX(2px)' },
  { transform: 'translateX(0)' },
];

/** Shakes the element once when it turns invalid. Not on the first render, not with reduced motion. */
export function useShake(ref: React.RefObject<HTMLElement | null>, invalid: boolean) {
  const was = useRef(invalid);
  useEffect(() => {
    const turned = invalid && !was.current;
    was.current = invalid;
    const el = ref.current;
    if (!turned || !el || prefersReducedMotion() || typeof el.animate !== 'function') return;
    el.animate(SHAKE, { duration: 420, easing: 'ease-out' });
  }, [invalid, ref]);
}

/*
 * What is on the page from the start just stands there; what arrives later
 * (an error after typing, a check after a valid entry) comes in with a short
 * movement. The first control effect marks the moment the page came alive.
 */
let alive = 0;
/** ms since the first form control on this page ran its effects */
export function sinceAlive() {
  const now = performance.now();
  if (!alive) alive = now;
  return now - alive;
}

/** Rises in a few pixels when it mounts after the page came alive */
export function useArrival<T extends HTMLElement>() {
  const ref = useRef<T>(null);
  useEffect(() => {
    const el = ref.current;
    if (sinceAlive() < 400 || !el || prefersReducedMotion() || typeof el.animate !== 'function') return;
    el.animate([{ transform: 'translateY(-4px)', opacity: 0 }, { transform: 'translateY(0)', opacity: 1 }], { duration: 320, easing: EASE });
  }, []);
  return ref;
}

/** A check that draws itself (stroke-dashoffset) when it appears after the page came alive */
export function SuccessMark({ size = 16, className }: { size?: number; className?: string }) {
  const ref = useRef<SVGPathElement>(null);
  useEffect(() => {
    const el = ref.current;
    if (sinceAlive() < 400 || !el || prefersReducedMotion() || typeof el.animate !== 'function') return;
    el.animate([{ strokeDashoffset: 1 }, { strokeDashoffset: 1, offset: 0.15 }, { strokeDashoffset: 0 }], { duration: 480, easing: EASE });
  }, []);
  return (
    <svg viewBox="0 0 24 24" width={size} height={size} fill="none" aria-hidden className={cn('shrink-0 text-success', className)}>
      <path ref={ref} d="m5 12.5 4.5 4.5L19 7.5" pathLength={1} strokeDasharray="1 1" strokeDashoffset={0} stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

/* ------------------------------------------------------------------ */
/* Field                                                               */
/* ------------------------------------------------------------------ */

interface FieldState {
  id: string;
  describedBy?: string;
  invalid: boolean;
  disabled?: boolean;
  required?: boolean;
}

const FieldContext = createContext<FieldState | null>(null);

/** The props a control needs to be wired to its field. Works without a field too. */
export function useControl(props: { id?: string; invalid?: boolean; disabled?: boolean; required?: boolean; 'aria-describedby'?: string }) {
  const field = useContext(FieldContext);
  const auto = useId();
  const invalid = props.invalid ?? field?.invalid ?? false;
  return {
    id: props.id ?? field?.id ?? auto,
    'aria-describedby': cn(field?.describedBy, props['aria-describedby']) || undefined,
    'aria-invalid': invalid || undefined,
    disabled: props.disabled ?? field?.disabled,
    required: props.required ?? field?.required,
    invalid,
  };
}

export interface FieldOwnProps {
  label?: React.ReactNode;
  /** Help text under the control */
  hint?: React.ReactNode;
  /** Error as text. Replaces the hint and marks the control invalid. */
  error?: React.ReactNode;
  /** Shows "Optional" next to the label */
  optional?: boolean;
  /** Something at the right end of the label row, e.g. a "Forgot password?" link */
  labelAction?: React.ReactNode;
}

export interface FieldProps extends FieldOwnProps {
  id?: string;
  required?: boolean;
  disabled?: boolean;
  className?: string;
  children: React.ReactNode;
}

export function Label({ htmlFor, required, optional, className, children, id }: { htmlFor?: string; required?: boolean; optional?: boolean; className?: string; children: React.ReactNode; id?: string }) {
  const t = useTexts(TEXTS);
  return (
    <label id={id} htmlFor={htmlFor} className={cn('inline-flex items-baseline gap-1.5 text-sm font-medium leading-5 text-foreground', className)}>
      <span>
        {children}
        {required && (
          <span aria-hidden className="ms-0.5 text-[color-mix(in_oklab,var(--destructive)_85%,var(--foreground))]">
            *
          </span>
        )}
      </span>
      {optional && <span className="text-xs font-normal text-subtle-foreground">{t.optional}</span>}
    </label>
  );
}

export function FieldHint({ id, className, children }: { id?: string; className?: string; children: React.ReactNode }) {
  return (
    <p id={id} className={cn('text-[13px] leading-5 text-muted-foreground', className)}>
      {children}
    </p>
  );
}

export function FieldError({ id, className, children }: { id?: string; className?: string; children: React.ReactNode }) {
  const ref = useArrival<HTMLParagraphElement>();
  return (
    <p ref={ref} id={id} className={cn('flex items-start gap-1.5 text-[13px] font-medium leading-5 text-destructive', className)}>
      <Icon name="alert-circle" size={15} strokeWidth={1.8} className="mt-[2.5px]" />
      <span>{children}</span>
    </p>
  );
}

/** Label, control, hint or error, wired together. */
export function Field({ id, label, hint, error, optional, labelAction, required, disabled, className, children }: FieldProps) {
  const auto = useId();
  const fid = id ?? auto;
  const messageId = `${fid}-message`;
  const hasMessage = Boolean(error || hint);
  return (
    <FieldContext.Provider value={{ id: fid, describedBy: hasMessage ? messageId : undefined, invalid: Boolean(error), disabled, required }}>
      <div className={cn('grid w-full min-w-0 grid-cols-[minmax(0,1fr)] content-start gap-2', disabled && 'opacity-80', className)}>
        {(label || labelAction) && (
          <div className="flex min-h-5 items-baseline justify-between gap-3">
            {label && (
              <Label htmlFor={fid} required={required} optional={optional}>
                {label}
              </Label>
            )}
            {labelAction && <div className="text-[13px]">{labelAction}</div>}
          </div>
        )}
        {children}
        <div aria-live="polite" className="empty:hidden">
          {error ? <FieldError id={messageId}>{error}</FieldError> : hint ? <FieldHint id={messageId}>{hint}</FieldHint> : null}
        </div>
      </div>
    </FieldContext.Provider>
  );
}

/** Wraps a control in a Field when label, hint or error are given */
export function withField(props: FieldOwnProps & { id?: string; required?: boolean; disabled?: boolean; className?: string }, control: React.ReactNode) {
  const { label, hint, error, optional, labelAction, id, required, disabled, className } = props;
  if (!label && !hint && !error && !labelAction) return control;
  return (
    <Field {...{ label, hint, error, optional, labelAction, id, required, disabled, className }}>
      {control}
    </Field>
  );
}

/* ------------------------------------------------------------------ */
/* Input                                                               */
/* ------------------------------------------------------------------ */

export interface InputProps extends FieldOwnProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix'> {
  size?: ControlSize;
  /** Icon at the start */
  icon?: IconName;
  /** Quiet text inside the box, before the value, e.g. "https://" */
  prefix?: React.ReactNode;
  /** Quiet text inside the box, after the value, e.g. "€" or "kg" */
  suffix?: React.ReactNode;
  /** A boxed segment attached at the start, e.g. a protocol or a country code */
  startAddon?: React.ReactNode;
  /** A boxed segment attached at the end, e.g. a domain */
  endAddon?: React.ReactNode;
  /** Any element at the end inside the box, e.g. a button, a key hint or a spinner */
  trailing?: React.ReactNode;
  /** Marks the control invalid without an error text (use error for text). The box shakes once when it turns invalid. */
  invalid?: boolean;
  /** Marks the value as accepted: a green edge and a check that draws itself */
  success?: boolean;
  /**
   * The label sits inside the box like a placeholder and floats up when the
   * field has focus or a value. The box is 56 px tall. Off by default.
   */
  floatingLabel?: boolean;
  ref?: React.Ref<HTMLInputElement>;
  /** Class of the outer element (field or box) */
  className?: string;
  inputClassName?: string;
}

function Addon({ side, children }: { side: 'start' | 'end'; children: React.ReactNode }) {
  return (
    <span className={cn('flex h-full shrink-0 items-center bg-surface-2 px-3 text-muted-foreground', side === 'start' ? 'rounded-s-[inherit] border-e border-input' : 'rounded-e-[inherit] border-s border-input')}>
      {children}
    </span>
  );
}

const SUCCESS_BOX =
  'border-[color-mix(in_oklab,var(--success)_55%,var(--border))] hover:border-[color-mix(in_oklab,var(--success)_65%,var(--border))] focus-within:border-[color-mix(in_oklab,var(--success)_70%,var(--border-strong))] focus-within:shadow-[0_0_0_3px_color-mix(in_oklab,var(--success)_16%,transparent),var(--shadow-sm)]';
const MARGIN_END: Record<ControlSize, string> = { sm: 'me-3', md: 'me-3.5', lg: 'me-4' };

function InputBox({
  size = 'md',
  icon,
  prefix,
  suffix,
  startAddon,
  endAddon,
  trailing,
  invalid,
  success,
  floating,
  className,
  inputClassName,
  ref,
  ...rest
}: Omit<InputProps, keyof FieldOwnProps | 'floatingLabel'> & { floating?: { label: React.ReactNode } }) {
  const c = useControl({ ...rest, invalid });
  const boxRef = useRef<HTMLDivElement>(null);
  useShake(boxRef, c.invalid);
  const ok = Boolean(success) && !c.invalid;
  const lead = Boolean(icon || prefix) && !startAddon;
  const tail = Boolean(suffix || trailing || ok) && !endAddon;
  const padStart = lead ? (prefix ? 'ps-1' : 'ps-2.5') : startAddon ? 'ps-3' : PAD_START[size];
  const input = (
    <input
      {...rest}
      ref={ref}
      id={c.id}
      disabled={c.disabled}
      required={c.required}
      aria-invalid={c['aria-invalid']}
      aria-describedby={c['aria-describedby']}
      placeholder={floating ? rest.placeholder ?? ' ' : rest.placeholder}
      className={cn(
        'h-full w-0 min-w-0 flex-1 bg-transparent text-inherit outline-none placeholder:text-subtle-foreground disabled:cursor-not-allowed',
        '[&::-webkit-search-cancel-button]:appearance-none',
        padStart,
        tail ? 'pe-2' : endAddon ? 'pe-3' : PAD_END[size],
        floating && 'peer pt-[18px] placeholder:text-transparent focus:placeholder:text-subtle-foreground',
        inputClassName,
      )}
    />
  );
  return (
    <div
      ref={boxRef}
      data-invalid={c.invalid || undefined}
      data-disabled={c.disabled || undefined}
      data-success={ok || undefined}
      className={cn(
        // No overflow-hidden: it would clip the 44 px hit area of a button inside (password eye). The addons round their own corners.
        'group/input relative flex w-full min-w-0 items-center',
        floating ? 'h-14 rounded-lg text-base sm:text-[15px]' : CONTROL_HEIGHT[size],
        controlBox,
        ok && SUCCESS_BOX,
        className,
      )}
    >
      {startAddon && <Addon side="start">{startAddon}</Addon>}
      {icon && (
        <Icon
          name={icon}
          size={ICON_SIZE[size]}
          className={cn('shrink-0 text-subtle-foreground transition-colors duration-300 group-focus-within/input:text-muted-foreground', startAddon ? 'ms-3' : MARGIN_START[size])}
        />
      )}
      {prefix && <span className={cn('shrink-0 select-none text-subtle-foreground', icon ? 'ps-2' : startAddon ? 'ps-3' : PAD_START[size], floating && 'pt-[18px]')}>{prefix}</span>}
      {floating ? (
        <span className="relative flex h-full min-w-0 flex-1">
          {input}
          <label
            htmlFor={c.id}
            className={cn(
              'pointer-events-none absolute inset-y-0 start-0 flex max-w-full origin-[0_50%] items-center truncate text-subtle-foreground transition-[translate,scale,color] duration-300 ease-[var(--ease-kit)] motion-reduce:transition-none',
              padStart,
              'peer-focus:-translate-y-[11px] peer-focus:scale-[0.8] peer-focus:text-[color-mix(in_oklab,var(--primary)_75%,var(--foreground))]',
              'peer-[:not(:placeholder-shown)]:-translate-y-[11px] peer-[:not(:placeholder-shown)]:scale-[0.8] peer-[:not(:placeholder-shown)]:text-muted-foreground',
            )}
          >
            {floating.label}
            {c.required && (
              <span aria-hidden className="ms-0.5 text-[color-mix(in_oklab,var(--destructive)_85%,var(--foreground))]">
                *
              </span>
            )}
          </label>
        </span>
      ) : (
        input
      )}
      {ok && !endAddon && <SuccessMark size={ICON_SIZE[size] - 1} className={suffix || trailing ? 'me-2' : MARGIN_END[size]} />}
      {suffix && <span className={cn('shrink-0 select-none text-subtle-foreground', trailing ? 'pe-2' : PAD_END[size], floating && 'pt-[18px]')}>{suffix}</span>}
      {trailing && <span className={cn('flex shrink-0 items-center', size === 'sm' ? 'pe-1' : 'pe-1.5')}>{trailing}</span>}
      {endAddon && <Addon side="end">{endAddon}</Addon>}
    </div>
  );
}

/**
 * Text input with label, hint, error, icon, inline prefix/suffix and attached addons.
 * The focus ring fades in, an error shakes the box once, success draws a check.
 *   <Input label="Email" type="email" icon="mail" hint="We never share it." />
 *   <Input label="Website" startAddon="https://" placeholder="northwind.io" />
 *   <Input label="Price" suffix="€" error="Enter a price above 0." />
 *   <Input label="Email" floatingLabel />
 */
export function Input({ label, hint, error, optional, labelAction, floatingLabel, className, ...rest }: InputProps) {
  if (floatingLabel && label) {
    const wrapped = Boolean(hint || error || labelAction);
    return withField(
      { hint, error, labelAction, id: rest.id, required: rest.required, disabled: rest.disabled, className },
      <InputBox {...rest} floating={{ label }} className={wrapped ? undefined : className} />,
    );
  }
  return withField({ label, hint, error, optional, labelAction, id: rest.id, required: rest.required, disabled: rest.disabled, className }, <InputBox {...rest} className={label || hint || error ? undefined : className} />);
}

/** A small square button inside a control, e.g. clear or reveal. A new icon turns in with a short spin. */
export function InlineButton({ label, icon, onClick, className, pressed }: { label: string; icon: IconName; onClick?: () => void; className?: string; pressed?: boolean }) {
  const ref = useRef<HTMLButtonElement>(null);
  const last = useRef(icon);
  useEffect(() => {
    if (last.current === icon) return;
    last.current = icon;
    const svg = ref.current?.querySelector('svg');
    if (!svg || prefersReducedMotion() || typeof svg.animate !== 'function') return;
    svg.animate([{ transform: 'rotate(-60deg) scale(0.5)', opacity: 0 }, { transform: 'rotate(0) scale(1)', opacity: 1 }], { duration: 320, easing: EASE });
  }, [icon]);
  return (
    <button
      ref={ref}
      type="button"
      aria-label={label}
      aria-pressed={pressed}
      onClick={onClick}
      className={cn(
        'relative grid size-8 place-items-center rounded-md text-muted-foreground outline-none transition-[background-color,color,scale] duration-200 hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-ring active:scale-90 active:duration-100 motion-reduce:active:scale-100',
        'after:absolute after:-inset-1.5 after:content-[""]',
        className,
      )}
    >
      <Icon name={icon} size={16} />
    </button>
  );
}

/** Password input with a show/hide button */
export function PasswordInput(props: Omit<InputProps, 'type' | 'trailing'>) {
  const t = useTexts(TEXTS);
  const [visible, setVisible] = useState(false);
  return (
    <Input
      autoComplete="current-password"
      {...props}
      type={visible ? 'text' : 'password'}
      trailing={<InlineButton label={visible ? t.hide : t.show} icon={visible ? 'eye-off' : 'eye'} onClick={() => setVisible((v) => !v)} />}
    />
  );
}

Häufige Fragen

Wie installiere ich Eingabefeld?

Mit aiw add @ai-website/input. Der Befehl legt die Datei components/ui/input.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Eingabefeld?

4 Varianten, Zustände, Icons und Anhängsel, Größen und Passwort.

Kann ich die Texte von Eingabefeld übersetzen?

Ja. Eingabefeld bringt 3 eigene Texte auf Deutsch und Englisch mit, etwa „Optional“, „Passwort zeigen“ und „Passwort verbergen“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.

Kann ein KI-Assistent Eingabefeld einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Eingabefeld mit aiw_components und installieren sie mit aiw_add @ai-website/input.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert