Campo de chips claymorfismo
Escribe y pulsa Enter o coma para añadir, Retroceso borra el último; chips blandos que se quitan con su botón.
AutomatizaciónReportes
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useId, useRef, useState, type CSSProperties, type KeyboardEvent } from "react";
/** Campo de chips: escribe y pulsa Enter o coma para añadir, Retroceso borra el último, cada chip se quita con su botón. Estilo claymorfismo suave. React + TypeScript, sin dependencias. */
export type BaseChipsProps = {
label?: string;
defaultValue?: string[];
suggestions?: string[];
max?: number;
onChange?: (values: string[]) => void;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcbch-root { box-sizing: border-box; width: 100%; max-width: 440px; color: #40284d; font: 700 15px/1.3 ui-rounded, "SF Pro Rounded", "Nunito", ui-sans-serif, system-ui, sans-serif; }
.gcbch-root *, .gcbch-root *::before, .gcbch-root *::after { box-sizing: border-box; }
.gcbch-root button, .gcbch-root input { font: inherit; }
.gcbch-root :focus-visible { outline: 3px solid #40284d; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .gcbch-root * { animation: none !important; transition-duration: .01ms !important; } }
.gcbch-label { display: block; margin: 0 0 10px 6px; font-size: 14px; }
.gcbch-box { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; min-height: 64px; padding: 12px; border-radius: 22px; background: #fff0e6; box-shadow: inset 5px 6px 10px rgba(190, 120, 80, .28), inset -5px -6px 10px rgba(255, 255, 255, .9); cursor: text; }
.gcbch-chip { display: inline-flex; align-items: center; gap: 4px; height: 36px; padding: 0 6px 0 14px; border-radius: 999px; background: #c3b1ff; color: #2e2160; box-shadow: inset -3px -4px 6px rgba(80, 50, 160, .3), inset 3px 4px 6px rgba(255, 255, 255, .55), 2px 4px 7px -2px rgba(80, 50, 160, .45); animation: gcbch-in .35s cubic-bezier(.34, 1.56, .64, 1); }
.gcbch-x { display: grid; place-items: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 50%; background: rgba(255, 255, 255, .4); color: inherit; cursor: pointer; transition: background .15s; }
.gcbch-x:hover { background: rgba(255, 255, 255, .8); }
.gcbch-input { flex: 1; min-width: 120px; height: 36px; border: 0; background: transparent; color: inherit; outline: 0; }
.gcbch-input::placeholder { color: #a98aa0; font-weight: 600; }
.gcbch-sug { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.gcbch-add { height: 34px; padding: 0 14px; border: 0; border-radius: 999px; background: #ffd9c4; color: #6a3a22; cursor: pointer; font-size: 14px; box-shadow: inset -2px -3px 5px rgba(190, 90, 50, .25), inset 2px 3px 5px rgba(255, 255, 255, .7); transition: transform .25s cubic-bezier(.34, 1.56, .64, 1); }
.gcbch-add:hover { transform: translateY(-2px) scale(1.04); }
.gcbch-live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
@keyframes gcbch-in { from { transform: scale(.4); opacity: 0; } }`;
export function BaseChips({ label = "Etiquetas del proyecto", defaultValue = ["Automatización", "Reportes"], suggestions = ["Integraciones", "Seguridad", "Nube", "Móvil"], max = 8, onChange, className = "", style }: BaseChipsProps) {
const uid = useId();
const [values, setValues] = useState(defaultValue);
const [text, setText] = useState("");
const [live, setLive] = useState("");
const input = useRef<HTMLInputElement>(null);
function commit(next: string[], message: string) {
setValues(next);
setLive(message);
onChange?.(next);
}
function add(raw: string) {
const value = raw.trim().replace(/,$/, "").trim();
if (!value || values.some((v) => v.toLowerCase() === value.toLowerCase()) || values.length >= max) { setText(""); return; }
commit([...values, value], `Se añadió ${value}`);
setText("");
}
function remove(value: string) {
commit(values.filter((v) => v !== value), `Se quitó ${value}`);
input.current?.focus();
}
function key(event: KeyboardEvent<HTMLInputElement>) {
if (event.key === "Enter" || event.key === ",") { event.preventDefault(); add(text); }
else if (event.key === "Backspace" && !text && values.length) remove(values[values.length - 1]);
}
return (
<div className={`gcbch-root ${className}`} style={style}>
<style>{CSS}</style>
<label className="gcbch-label" htmlFor={uid}>{label}</label>
<div className="gcbch-box" onClick={() => input.current?.focus()}>
{values.map((value) => (
<span key={value} className="gcbch-chip">
{value}
<button className="gcbch-x" type="button" aria-label={`Quitar ${value}`} onClick={() => remove(value)}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18" /></svg>
</button>
</span>
))}
<input id={uid} ref={input} className="gcbch-input" value={text} placeholder={values.length >= max ? `Máximo ${max}` : "Escribe y pulsa Enter"} disabled={values.length >= max} onChange={(event) => setText(event.target.value)} onKeyDown={key} onBlur={() => text && add(text)} />
</div>
<div className="gcbch-sug">
{suggestions.filter((s) => !values.includes(s)).map((suggestion) => (
<button key={suggestion} className="gcbch-add" type="button" onClick={() => add(suggestion)}>
+ {suggestion}
</button>
))}
</div>
<span className="gcbch-live" role="status" aria-live="polite">{live}</span>
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/base-chipsEjemplo de uso de Campo de chips claymorfismo
Después de copiar base-chips.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { BaseChips } from "./base-chips";
export default function Ejemplo() {
return <BaseChips label="Ejemplo" />;
}Propiedades de BaseChips
BaseChips acepta 7 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
label? | string | Opcional. |
defaultValue? | string[] | Opcional. |
suggestions? | string[] | Opcional. |
max? | number | Opcional. |
onChange? | (values: string[]) => void | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 88 líneas (5 KB) y define 1 animación con @keyframes. Esto es lo que hace Campo de chips claymorfismo 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).
- Declara roles ARIA para lectores de pantalla: status.
- Responde al teclado, no solo al ratón.
- Es un componente de cliente de Next.js: incluye la directiva "use client".