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.
"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.
npx shadcn@latest add https://www.gassierconsulting.com/r/campo-neumorficoEjemplo de uso de Campo neumórfico
Después de copiar neumorphic-field.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
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.
| Variable | Valor 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".
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.