HextaUI

Input

Un champ de saisie texte en trois tailles, avec états invalide et lecture seule, style de validation natif et police tactile de 16px pour que les téléphones ne zooment jamais.

import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export function InputDemo() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-2">
      <Label htmlFor="input-demo-email">Email</Label>
      <Input
        id="input-demo-email"
        type="email"
        autoComplete="email"
        placeholder="[email protected]"
      />
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/input.json

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

import { Input } from "@/components/ui/input"
<label htmlFor="email">Email</label>
<Input id="email" type="email" placeholder="[email protected]" />

Tailles

sm, default et lg correspondent aux hauteurs des boutons : un champ et un bouton de même taille s’alignent donc sur une ligne.

import { Input } from "@/components/ui/input"

export function InputSizes() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <Input size="sm" aria-label="Small" placeholder="Small" />
      <Input aria-label="Default" placeholder="Default" />
      <Input size="lg" aria-label="Large" placeholder="Large" />
    </div>
  )
}

Avec une description

Pointez aria-describedby vers le texte d’aide pour que les lecteurs d’écran le lisent après le libellé.

import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export function InputDescription() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-2">
      <Label htmlFor="input-username">Username</Label>
      <Input
        id="input-username"
        autoComplete="username"
        placeholder="preet"
        aria-describedby="input-username-description"
      />
      <p
        id="input-username-description"
        className="text-sm text-muted-foreground"
      >
        Shown on your profile and in mentions.
      </p>
    </div>
  )
}

Invalide

aria-invalid colore en rouge le bord et l’anneau de focus. Reliez le message avec aria-describedby pour qu’il soit annoncé, et pas seulement coloré.

import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export function InputInvalid() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-2">
      <Label htmlFor="input-invalid">Email</Label>
      <Input
        id="input-invalid"
        type="email"
        defaultValue="preet@"
        aria-invalid
        aria-describedby="input-invalid-error"
      />
      <p id="input-invalid-error" className="text-sm text-destructive">
        Enter a full email address, like [email protected].
      </p>
    </div>
  )
}

Validation native

Les champs avec required, type="email" ou pattern ne passent au rouge qu’après qu’on y a saisi quelque chose ou tenté de soumettre, jamais au premier rendu. Une soumission qui trouve un champ invalide le fait vibrer une fois, pour que le regard se pose sur ce qu’il faut corriger. Il ne vibre jamais pendant la saisie ni la tabulation. Soumettez le formulaire vide pour le voir.

import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export function InputNativeValidation() {
  return (
    <form className="flex w-full max-w-sm flex-col gap-3">
      <div className="flex flex-col gap-2">
        <Label htmlFor="input-native-email">Email</Label>
        <Input
          id="input-native-email"
          name="email"
          type="email"
          required
          placeholder="[email protected]"
        />
      </div>
      <div className="flex flex-col gap-2">
        <Label htmlFor="input-native-code">Invite code</Label>
        <Input
          id="input-native-code"
          name="code"
          required
          pattern="[A-Z]{4}-[0-9]{4}"
          placeholder="ABCD-1234"
        />
      </div>
      <Button type="submit" className="self-start">
        Join
      </Button>
    </form>
  )
}

Désactivé

Un champ désactivé ne peut être ni focalisé, ni modifié, ni soumis avec le formulaire.

import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export function InputDisabled() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-2">
      <Label htmlFor="input-disabled">Workspace</Label>
      <Input id="input-disabled" defaultValue="Acme Inc." disabled />
    </div>
  )
}

Lecture seule

readOnly garde la valeur focusable, sélectionnable et soumise, avec une surface atténuée pour qu’elle n’ait pas l’air modifiable. Préférez-le à disabled pour les valeurs que l’on doit pouvoir copier.

import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export function InputReadOnly() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-2">
      <Label htmlFor="input-read-only">API key</Label>
      <Input id="input-read-only" readOnly defaultValue="sk_live_51H8a…f2Qz" />
    </div>
  )
}

Fichier

type="file" reçoit le même cadre, avec le bouton du navigateur restylé en texte simple.

import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export function InputFile() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-2">
      <Label htmlFor="input-file">Avatar</Label>
      <Input id="input-file" type="file" accept="image/*" />
    </div>
  )
}

Types de champ

Mot de passe, nombre, recherche, date et heure partagent une même hauteur et un même cadre. En mode sombre, les sélecteurs et les compteurs du navigateur passent aussi en sombre.

import { Input } from "@/components/ui/input"

export function InputTypes() {
  return (
    <div className="grid w-full max-w-sm gap-3">
      <Input type="password" aria-label="Password" defaultValue="hunter2" />
      <Input type="number" aria-label="Seats" defaultValue={12} min={1} />
      <Input type="search" aria-label="Search" placeholder="Search…" />
      <Input type="date" aria-label="Start date" defaultValue="2026-10-03" />
      <Input type="time" aria-label="Start time" defaultValue="09:30" />
    </div>
  )
}

Contrôlé

onValueChange vous donne directement la chaîne : pas de event.target.value à extraire. onChange fonctionne toujours aussi.

"use client"

import * as React from "react"

import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

const limit = 32

export function InputControlled() {
  const [value, setValue] = React.useState("Quarterly planning")

  return (
    <div className="flex w-full max-w-sm flex-col gap-2">
      <Label htmlFor="input-controlled">Project name</Label>
      <Input
        id="input-controlled"
        value={value}
        maxLength={limit}
        onValueChange={setValue}
        aria-describedby="input-controlled-count"
      />
      <p
        id="input-controlled-count"
        className="text-end text-sm text-muted-foreground tabular-nums"
      >
        {value.length}/{limit}
      </p>
    </div>
  )
}

Avec un bouton

Côte à côte avec un écart, ou réunis en un seul contrôle dans un <ButtonGroup />, où le champ prend la largeur restante.

import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Input } from "@/components/ui/input"

export function InputWithButton() {
  return (
    <form className="flex w-full max-w-sm flex-col gap-4">
      <div className="flex gap-2">
        <Input type="email" aria-label="Email" placeholder="[email protected]" />
        <Button type="submit">Subscribe</Button>
      </div>
      <ButtonGroup className="w-full">
        <Input type="search" aria-label="Search" placeholder="Search…" />
        <Button variant="outline">Search</Button>
      </ButtonGroup>
    </form>
  )
}

Grille

Les champs remplissent leur conteneur : placez-les donc dans une grille. Donnez min-w-0 aux cellules de la grille pour que de longues valeurs ne puissent pas étirer une colonne.

import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export function InputGrid() {
  return (
    <div className="grid w-full max-w-sm grid-cols-2 gap-3">
      <div className="flex min-w-0 flex-col gap-2">
        <Label htmlFor="input-first-name">First name</Label>
        <Input id="input-first-name" autoComplete="given-name" />
      </div>
      <div className="flex min-w-0 flex-col gap-2">
        <Label htmlFor="input-last-name">Last name</Label>
        <Input id="input-last-name" autoComplete="family-name" />
      </div>
    </div>
  )
}

Contenu long

Les longues valeurs défilent dans le champ et les longs placeholders sont coupés, sans élargir la mise en page.

import { Input } from "@/components/ui/input"

export function InputLongContent() {
  return (
    <div className="flex w-full max-w-56 flex-col gap-3">
      <Input
        aria-label="URL"
        defaultValue="https://example.com/a/really/long/url/without/any/spaces/at/all"
      />
      <Input
        aria-label="Note"
        placeholder="A placeholder that is far too long to fit in this field"
      />
    </div>
  )
}

De droite à gauche

Le texte, le curseur et le remplissage suivent la direction. Utilisez dir="auto" sur les champs qui contiennent des valeurs de gauche à droite, comme une adresse e-mail dans un formulaire en arabe.

import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export function InputRtl() {
  return (
    <div dir="rtl" className="flex w-full max-w-sm flex-col gap-2">
      <Label htmlFor="input-rtl">البريد الإلكتروني</Label>
      <Input id="input-rtl" placeholder="[email protected]" dir="auto" />
      <Input aria-label="الاسم" placeholder="اكتب اسمك" />
    </div>
  )
}
  • Chaque champ a besoin d’un nom. Utilisez un <label> avec htmlFor, ou aria-label lorsqu’il n’y a pas de libellé visible. Un placeholder n’est pas un libellé.
  • Reliez le texte d’aide et d’erreur avec aria-describedby, et ne définissez aria-invalid que lorsqu’il y a une erreur à afficher.
  • Sur écran tactile, le texte fait au moins 16 px, pour qu’iOS Safari ne zoome pas sur la page lorsque le champ reçoit le focus.
  • Dans un Field de Base UI, le libellé, la description, l’erreur et la validité sont câblés pour vous.

Construit sur l’input de Base UI. Il accepte tous les attributs d’input natifs.

PropTypePar défaut
sizeHauteur et remplissage, alignés sur les boutons.
"sm" | "default" | "lg""default"
htmlSizeL’attribut size natif, renommé car size est la variante.
number–
value
string | number | string[]–
defaultValue
string | number | string[]–
onValueChangeAppelé avec la nouvelle valeur à chaque changement.
(value: string, details) => void–
type
string"text"
disabled
booleanfalse
readOnly
booleanfalse
aria-invalidAffiche le bord invalide et l’anneau de focus.
boolean–
className
string | (state) => string–
shakeVibre une fois lorsqu’une soumission de formulaire trouve ce champ invalide. Fonctionne avec la validation native, le Field de Base UI et les bibliothèques qui définissent aria-invalid. Ignoré avec la réduction des animations.
booleantrue
render
ReactElement | (props, state) => ReactElement<input>
AttributDescription
data-slot="input"Ciblez le champ en CSS.
data-sizeLa taille actuelle.
data-shakePrésent pendant que le champ vibre après une soumission échouée.
data-disabledPrésent lorsque le champ est désactivé.
data-invalidPrésent lorsque le Field englobant est invalide. Stylisé comme aria-invalid.
data-validPrésent lorsque le Field englobant est valide.
data-touchedPrésent après que le champ a perdu le focus une fois, dans un Field.
data-dirtyPrésent une fois la valeur modifiée, dans un Field.
data-filledPrésent lorsque le champ a une valeur, dans un Field.
data-focusedPrésent tant qu’il a le focus, dans un Field.

Les noms de classes derrière le champ, pour styliser un autre élément à l’identique, comme un <select> ou un <textarea> natif. Appelez-le avec { size }.

Le décompte de caractères derrière <InputGroupCount /> et <FieldCounter />. Utilisez ces parties, qui lisent le champ pour vous. Ne recourez à celle-ci que lorsque vous suivez vous-même la longueur.

PropTypePar défaut
lengthRequis.
number–
maxLength
number | null–
threshold
number10% of maxLength, at most 20
announcement
(remaining: number) => string–
AttributDescription
data-slot="input-count"Ciblez le compteur en CSS.
data-state="near" | "limit"Présent dans le seuil, et à la limite.

Utilisé dans les blocks

Des blocks qui s’appuient sur Input.