Ir al contenido
Evaluar mi proyecto
Componentes · Componentes baseDisponible

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.

base-chips.tsx88 líneas
Descargar
"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.

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

Ejemplo 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.

ejemplo.tsx5 líneas
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.

PropiedadTipoDescripción
label?stringOpcional.
defaultValue?string[]Opcional.
suggestions?string[]Opcional.
max?numberOpcional.
onChange?(values: string[]) => voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

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".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)