Ir al contenido
Evaluar mi proyecto
Componentes · FormulariosDisponible

Campo de texto Material Design 3

Campos filled y outlined con etiqueta flotante, texto de ayuda, contador y estado de error, según la guía de Material 3.

Te escribiremos solo para avisarte del pedido.
Como aparece en tu factura0/30
Faltan dígitos: usa 10 números.

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

campo-material.tsx91 líneas
Descargar
"use client";

import { useId, useState, type CSSProperties } from "react";

/** Campo de texto Material Design 3, relleno o con contorno, con etiqueta flotante, texto de ayuda y error. React + TypeScript, sin dependencias. */
export type CampoMaterialProps = {
  label?: string;
  variant?: "filled" | "outlined";
  value?: string;
  defaultValue?: string;
  onChange?: (value: string) => void;
  helper?: string;
  error?: string;
  type?: string;
  maxLength?: number;
  disabled?: boolean;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gccmt-root { box-sizing: border-box; display: block; width: 100%; max-width: 320px; font: 400 16px/24px Roboto, "Google Sans", ui-sans-serif, system-ui, sans-serif; color: #1d1b20; }
.gccmt-root *, .gccmt-root *::before, .gccmt-root *::after { box-sizing: border-box; }
.gccmt-root input { font: inherit; }
@media (prefers-reduced-motion: reduce) { .gccmt-root *, .gccmt-root *::before, .gccmt-root *::after { animation: none !important; transition-duration: .01ms !important; } }
.gccmt-field { --c: var(--gccmt-primary, #6750a4); position: relative; height: 56px; }
.gccmt-field[data-error="true"] { --c: var(--gccmt-error, #b3261e); }
.gccmt-input { position: absolute; inset: 0; width: 100%; height: 56px; padding: 24px 16px 8px; border: 0; background: transparent; color: inherit; caret-color: var(--c); outline: 0; }
.gccmt-input::placeholder { color: transparent; }
.gccmt-input:focus::placeholder { color: #79747e; }
.gccmt-label { position: absolute; left: 16px; top: 16px; max-width: calc(100% - 32px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #49454f; pointer-events: none; transform-origin: left top; transition: transform .2s cubic-bezier(.2, 0, 0, 1), color .2s; }
.gccmt-field[data-variant="filled"] { border-radius: 4px 4px 0 0; background: #e7e0ec; }
.gccmt-field[data-variant="filled"]::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: #49454f; transition: height .15s, background .15s; }
.gccmt-field[data-variant="filled"]:hover { background: color-mix(in srgb, #e7e0ec 92%, #1d1b20); }
.gccmt-field[data-variant="filled"][data-focus="true"]::after, .gccmt-field[data-variant="filled"][data-error="true"]::after { height: 2px; background: var(--c); }
.gccmt-field[data-variant="outlined"] .gccmt-input { padding: 16px; }
.gccmt-outline { position: absolute; inset: 0; border: 1px solid #79747e; border-radius: 4px; pointer-events: none; transition: border-color .15s; }
.gccmt-field[data-variant="outlined"]:hover .gccmt-outline { border-color: #1d1b20; }
.gccmt-field[data-variant="outlined"][data-focus="true"] .gccmt-outline, .gccmt-field[data-variant="outlined"][data-error="true"] .gccmt-outline { border-width: 2px; border-color: var(--c); }
.gccmt-field[data-variant="outlined"] .gccmt-label { padding: 0 4px; margin-left: -4px; background: var(--gccmt-surface, #fef7ff); }
.gccmt-field[data-up="true"] .gccmt-label { transform: translateY(-8px) scale(.75); }
.gccmt-field[data-variant="outlined"][data-up="true"] .gccmt-label { transform: translateY(-24px) scale(.75); }
.gccmt-field[data-focus="true"] .gccmt-label, .gccmt-field[data-error="true"] .gccmt-label { color: var(--c); }
.gccmt-field[data-disabled="true"] { opacity: .38; }
.gccmt-support { display: flex; justify-content: space-between; gap: 12px; padding: 4px 16px 0; font-size: 12px; line-height: 16px; color: #49454f; }
.gccmt-support[data-error="true"] { color: var(--gccmt-error, #b3261e); }`;

export function CampoMaterial({ label = "Correo electrónico", variant = "filled", value, defaultValue = "", onChange, helper = "Te escribiremos solo para avisarte del pedido.", error, type = "text", maxLength, disabled, className = "", style }: CampoMaterialProps) {
  const id = useId();
  const [inner, setInner] = useState(defaultValue);
  const [focus, setFocus] = useState(false);
  const current = value ?? inner;
  const note = error ?? helper;
  return (
    <div className={`gccmt-root ${className}`} style={style}>
      <style>{CSS}</style>
      <div className="gccmt-field" data-variant={variant} data-focus={focus} data-up={focus || current.length > 0} data-error={error ? "true" : undefined} data-disabled={disabled ? "true" : undefined}>
        <input
          id={id}
          className="gccmt-input"
          type={type}
          value={current}
          maxLength={maxLength}
          placeholder={variant === "outlined" ? " " : "name@example.com"}
          disabled={disabled}
          aria-invalid={error ? true : undefined}
          aria-describedby={note ? `${id}-note` : undefined}
          onFocus={() => setFocus(true)}
          onBlur={() => setFocus(false)}
          onChange={(event) => {
            setInner(event.target.value);
            onChange?.(event.target.value);
          }}
        />
        <label className="gccmt-label" htmlFor={id}>
          {label}
        </label>
        {variant === "outlined" ? <span className="gccmt-outline" /> : null}
      </div>
      <div className="gccmt-support" data-error={error ? "true" : undefined}>
        <span id={`${id}-note`}>{note}</span>
        {maxLength ? (
          <span>
            {current.length}/{maxLength}
          </span>
        ) : null}
      </div>
    </div>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/campo-material

Ejemplo de uso de Campo de texto Material Design 3

Después de copiar campo-material.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { CampoMaterial } from "./campo-material";

export default function Ejemplo() {
  return <CampoMaterial variant="filled" label="Ejemplo" />;
}

Propiedades de CampoMaterial

CampoMaterial acepta 12 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
label?stringOpcional.
variant?"filled" | "outlined"Opcional.
value?stringOpcional.
defaultValue?stringOpcional.
onChange?(value: string) => voidOpcional.
helper?stringOpcional.
error?stringOpcional.
type?stringOpcional.
maxLength?numberOpcional.
disabled?booleanOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Variantes disponibles

  • variant: filled, outlined

Personaliza Campo de texto Material Design 3 con variables CSS

Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.

VariableValor por defecto
--gccmt-primary#6750a4
--gccmt-error#b3261e
--gccmt-surface#fef7ff

Accesibilidad y rendimiento

El archivo tiene 91 líneas (5 KB) y no necesita @keyframes. Esto es lo que hace Campo de texto Material Design 3 por quien lo usa:

  • Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
  • Expone su estado a tecnologías de apoyo con atributos aria-*.
  • Calcula tonos con color-mix(), así que un solo color base alcanza.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)