Ir al contenido
Evaluar mi proyecto
Componentes · FormulariosDisponible

Campo neumórfico

Canal hundido en la superficie, con icono y botón para mostrar la contraseña.

Código completo

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

neumorphic-field.tsx77 líneas
Descargar
"use client";

/**
 * Campo neumórfico: un canal hundido en la superficie, con icono opcional y botón para mostrar la contraseña.
 * Como todo neumorfismo, funciona sobre un fondo del mismo color (#e0e5ec por defecto).
 *
 *   <NeumorphicField label="Correo" type="email" icon={<MailIcon />} />
 *   <NeumorphicField label="Contraseña" type="password" />
 *
 * Para otro fondo define `--gcf-bg`, `--gcf-shadow` y `--gcf-highlight` en un contenedor.
 */

import { useId, useState, type InputHTMLAttributes, type ReactNode } from "react";

export type NeumorphicFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size"> & {
  label: string;
  icon?: ReactNode;
  hint?: string;
  error?: string;
};

const CSS = `
.gcf-field { --gcf-ground: var(--gcf-bg, #e0e5ec); --gcf-dark: var(--gcf-shadow, #a3b1c6); --gcf-light: var(--gcf-highlight, #ffffff); --gcf-ink: #3b4658; --gcf-muted: #566379; --gcf-accent: #4f5bd5; --gcf-error: #c0392b;
  display: block; width: 100%; font-family: inherit; color: var(--gcf-ink); }
.gcf-label { display: block; margin: 0 0 10px 6px; color: var(--gcf-muted); font-size: 13px; font-weight: 600; letter-spacing: .02em; }
.gcf-well { display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 0 18px; border-radius: 16px; background: var(--gcf-ground);
  box-shadow: inset 6px 6px 12px var(--gcf-dark), inset -6px -6px 12px var(--gcf-light); transition: box-shadow .25s cubic-bezier(.16, 1, .3, 1); }
.gcf-well:focus-within { box-shadow: inset 8px 8px 16px var(--gcf-dark), inset -8px -8px 16px var(--gcf-light), 0 0 0 2px var(--gcf-accent); }
.gcf-field[data-invalid="true"] .gcf-well { box-shadow: inset 6px 6px 12px var(--gcf-dark), inset -6px -6px 12px var(--gcf-light), 0 0 0 2px var(--gcf-error); }
.gcf-icon { display: inline-flex; flex: none; color: var(--gcf-muted); }
.gcf-icon svg { width: 20px; height: 20px; }
.gcf-input { flex: 1; min-width: 0; padding: 0; border: 0; outline: none; background: transparent; color: inherit; font-family: inherit; font-size: 16px; line-height: 1.4; }
.gcf-input::placeholder { color: #5a687d; }
.gcf-toggle { flex: none; padding: 6px; margin: -6px; border: 0; border-radius: 8px; background: none; color: var(--gcf-muted); cursor: pointer; line-height: 0; }
.gcf-toggle:hover { color: var(--gcf-accent); }
.gcf-toggle:focus-visible { outline: 2px solid var(--gcf-accent); outline-offset: 2px; }
.gcf-toggle svg { width: 20px; height: 20px; }
.gcf-note { margin: 9px 0 0 6px; color: var(--gcf-muted); font-size: 13px; line-height: 1.4; }
.gcf-field[data-invalid="true"] .gcf-note { color: var(--gcf-error); }
@media (prefers-reduced-motion: reduce) { .gcf-well { transition: none; } }
`;

const eye = (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12Z" /><circle cx="12" cy="12" r="3" /></svg>
);
const eyeOff = (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M3 3l18 18M10.6 5.1A10 10 0 0 1 12 5c6.4 0 10 7 10 7a17 17 0 0 1-3.2 4M6.5 6.6C3.7 8.4 2 12 2 12s3.6 7 10 7c1.6 0 3-.4 4.3-1M9.9 9.9a3 3 0 0 0 4.2 4.2" /></svg>
);

export function NeumorphicField({ label, icon, hint, error, type = "text", id, className, ...props }: NeumorphicFieldProps) {
  const auto = useId();
  const inputId = id ?? auto;
  const noteId = `${inputId}-note`;
  const note = error ?? hint;
  const [visible, setVisible] = useState(false);
  const isPassword = type === "password";

  return (
    <>
      <style>{CSS}</style>
      <div className={["gcf-field", className].filter(Boolean).join(" ")} data-invalid={error ? "true" : undefined}>
        <label className="gcf-label" htmlFor={inputId}>{label}</label>
        <div className="gcf-well">
          {icon ? <span className="gcf-icon" aria-hidden="true">{icon}</span> : null}
          <input {...props} id={inputId} className="gcf-input" type={isPassword && visible ? "text" : type} aria-invalid={error ? true : undefined} aria-describedby={note ? noteId : undefined} />
          {isPassword ? (
            <button type="button" className="gcf-toggle" onClick={() => setVisible((value) => !value)} aria-label={visible ? "Ocultar contraseña" : "Mostrar contraseña"} aria-pressed={visible}>
              {visible ? eyeOff : eye}
            </button>
          ) : null}
        </div>
        {note ? <p className="gcf-note" id={noteId} role={error ? "alert" : undefined}>{note}</p> : null}
      </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-neumorfico

Ejemplo de uso de Campo neumórfico

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

ejemplo.tsx5 líneas
import { NeumorphicField } from "./neumorphic-field";

export default function Ejemplo() {
  return <NeumorphicField />;
}

Personaliza Campo neumórfico 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
--gcf-bg#e0e5ec
--gcf-shadow#a3b1c6
--gcf-highlight#ffffff

Accesibilidad y rendimiento

El archivo tiene 77 líneas (5 KB) y no necesita @keyframes. Esto es lo que hace Campo neumórfico por quien lo usa:

  • Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
  • Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
  • Expone su estado a tecnologías de apoyo con atributos aria-*.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React · Next.js (quita la directiva use client fuera de Next.js) · Sin Tailwind

Acerca del producto

Un campo con relieve hundido (neumorfismo): el borde se ilumina al enfocar, admite un icono a la izquierda y, si es de tipo contraseña, un botón accesible para mostrarla u ocultarla.

Incluye

Un solo archivo React + TypeScript, con los estilos dentro. No usa Tailwind ni bibliotecas externas.

  • Icono opcional (pasa tu propio SVG)
  • Mostrar/ocultar contraseña con aria-pressed
  • Ayuda y error accesibles
  • Colores en variables CSS

Cómo usarlo

Colócalo sobre un fondo #e0e5ec o define --gcf-bg, --gcf-shadow y --gcf-highlight para tu propio fondo.