HextaUI

Input

Un campo de texto con tres tamaños, estados inválido y de solo lectura, estilos de validación nativos y una fuente táctil de 16px para que los móviles nunca hagan zoom.

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

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que depende.

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

Tamaños

sm, default y lg coinciden con las alturas de los botones, así que un input y un botón del mismo tamaño se alinean en una fila.

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>
  )
}

Con una descripción

Apunta aria-describedby al texto de ayuda para que los lectores de pantalla lo lean después de la etiqueta.

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>
  )
}

Inválido

aria-invalid pone el borde y el anillo de foco en rojo. Vincula el mensaje con aria-describedby para que se anuncie, no solo se coloree.

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>
  )
}

Validación nativa

Los campos con required, type="email" o pattern solo se ponen rojos después de que alguien haya escrito en ellos o intentado enviar, nunca en el primer renderizado. Un envío que encuentra un campo no válido lo sacude una vez, para que la vista se dirija a lo que hay que corregir. Nunca se sacude mientras escribes o tabulas. Envía el formulario vacío para verlo.

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>
  )
}

Deshabilitado

Un input deshabilitado no puede recibir foco, editarse ni enviarse con el formulario.

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>
  )
}

Solo lectura

readOnly mantiene el valor enfocable, seleccionable y enviado, con una superficie atenuada para que no parezca editable. Prefiérelo a disabled para valores que la gente necesita copiar.

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>
  )
}

Archivo

type="file" recibe el mismo marco, con el botón del navegador rediseñado como texto 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>
  )
}

Tipos de input

Password, number, search, date y time comparten una misma altura y marco. En modo oscuro, los selectores y spinners del navegador también pasan a oscuro.

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>
  )
}

Controlado

onValueChange te entrega la cadena directamente, así que no hay un event.target.value que desenvolver. onChange también sigue funcionando.

"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>
  )
}

Con un botón

Lado a lado con un hueco, o unidos en un solo control dentro de un <ButtonGroup />, donde el input ocupa el ancho 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>
  )
}

Cuadrícula

Los inputs llenan su contenedor, así que colócalos en una cuadrícula. Dale a las celdas de la cuadrícula min-w-0 para que los valores largos no puedan estirar una columna.

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>
  )
}

Contenido largo

Los valores largos se desplazan dentro del campo y los placeholders largos se cortan, sin ensanchar el diseño.

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 derecha a izquierda

El texto, el cursor y el relleno siguen la dirección. Usa dir="auto" en campos que contienen valores de izquierda a derecha, como una dirección de correo en un formulario en árabe.

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>
  )
}
  • Cada input necesita un nombre. Usa un <label> con htmlFor, o aria-label cuando no hay una etiqueta visible. Un placeholder no es una etiqueta.
  • Conecta el texto de ayuda y de error con aria-describedby, y define aria-invalid solo cuando haya un error que mostrar.
  • En pantallas táctiles el texto es de al menos 16px, para que iOS Safari no haga zoom en la página cuando el input recibe foco.
  • Dentro de un Field de Base UI, la etiqueta, la descripción, el error y la validez se conectan por ti.

Construido sobre el input de Base UI. Acepta todos los atributos nativos de input.

PropTipoPredeterminado
sizeAltura y relleno, a juego con los botones.
"sm" | "default" | "lg""default"
htmlSizeEl atributo size nativo, renombrado porque size es la variante.
number–
value
string | number | string[]–
defaultValue
string | number | string[]–
onValueChangeSe llama con el nuevo valor en cada cambio.
(value: string, details) => void–
type
string"text"
disabled
booleanfalse
readOnly
booleanfalse
aria-invalidMuestra el borde de no válido y el anillo de foco.
boolean–
className
string | (state) => string–
shakeSe sacude una vez cuando el envío de un formulario encuentra este input no válido. Funciona con la validación nativa, Field de Base UI y librerías que definen aria-invalid. Se omite con movimiento reducido.
booleantrue
render
ReactElement | (props, state) => ReactElement<input>
AtributoDescripción
data-slot="input"Selecciona el input en CSS.
data-sizeEl tamaño actual.
data-shakePresente mientras el input se sacude tras un envío fallido.
data-disabledPresente cuando el input está deshabilitado.
data-invalidPresente cuando el Field circundante no es válido. Con el mismo estilo que aria-invalid.
data-validPresente cuando el Field circundante es válido.
data-touchedPresente después de que el input perdió el foco una vez, dentro de un Field.
data-dirtyPresente una vez que el valor cambió, dentro de un Field.
data-filledPresente cuando el input tiene un valor, dentro de un Field.
data-focusedPresente mientras tiene foco, dentro de un Field.

Los nombres de clase detrás del input, para dar estilo a otro elemento a juego, como un <select> o <textarea> nativo. Llámalo con { size }.

El recuento de caracteres detrás de <InputGroupCount /> y <FieldCounter />. Usa esas partes, que leen el campo por ti. Recurre a este solo cuando lleves tú mismo la cuenta de la longitud.

PropTipoPredeterminado
lengthObligatorio.
number–
maxLength
number | null–
threshold
number10% of maxLength, at most 20
announcement
(remaining: number) => string–
AtributoDescripción
data-slot="input-count"Apunta al contador en CSS.
data-state="near" | "limit"Presente dentro del umbral y en el límite.

Usado en bloques

Bloques que se construyen sobre Input.