Ir al contenido
Evaluar mi proyecto
Componentes · FormulariosDisponible

Área de texto con anillo contador

Textarea que crece con el contenido y muestra caracteres restantes con un anillo que pasa a ámbar y a rojo.

280 caracteres disponibles

Código completo

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

area-texto-contador.tsx88 líneas
Descargar
"use client";

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

/** Área de texto que crece con el contenido y muestra el límite con un anillo que cambia de color. Estilo material oscuro. React + TypeScript, sin dependencias. */
export type AreaTextoContadorProps = {
  label?: string;
  placeholder?: string;
  maxLength?: number;
  value?: string;
  defaultValue?: string;
  onChange?: (value: string) => void;
  minRows?: number;
  maxRows?: number;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcatc-root { box-sizing: border-box; display: block; width: 100%; max-width: 420px; padding: 16px 18px 14px; border-radius: 18px; background: #1b1c22; color: #ececf1; font: 400 15px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; box-shadow: inset 0 0 0 1px #2c2e37; transition: box-shadow .2s; }
.gcatc-root:focus-within { box-shadow: inset 0 0 0 1.5px #7b8cff, 0 0 0 5px rgba(123, 140, 255, .16); }
.gcatc-root *, .gcatc-root *::before, .gcatc-root *::after { box-sizing: border-box; }
.gcatc-root textarea { font: inherit; }
@media (prefers-reduced-motion: reduce) { .gcatc-root *, .gcatc-root *::before, .gcatc-root *::after { transition-duration: .01ms !important; } }
.gcatc-label { display: block; margin-bottom: 6px; font-size: 12px; font-weight: 600; letter-spacing: .04em; color: #9da0b0; text-transform: uppercase; }
.gcatc-area { display: block; width: 100%; resize: none; border: 0; padding: 0; background: transparent; color: inherit; outline: 0; overflow-y: auto; }
.gcatc-area::placeholder { color: #6c6f80; }
.gcatc-area::selection { background: #7b8cff; color: #0d0e12; }
.gcatc-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; font-size: 12px; color: #9da0b0; font-variant-numeric: tabular-nums; }
.gcatc-ring { width: 24px; height: 24px; transform: rotate(-90deg); }
.gcatc-ring circle { fill: none; stroke-width: 3; }
.gcatc-ring .t { stroke: #2c2e37; }
.gcatc-ring .v { stroke: #7b8cff; stroke-linecap: round; transition: stroke-dashoffset .2s, stroke .2s; }
.gcatc-root[data-state="warn"] .gcatc-ring .v { stroke: #f5b44a; }
.gcatc-root[data-state="over"] .gcatc-ring .v { stroke: #ff6b6b; }
.gcatc-root[data-state="over"] .gcatc-count { color: #ff8a8a; }`;

export function AreaTextoContador({ label = "Cuéntanos tu proyecto", placeholder = "Qué haces hoy a mano y qué te gustaría que hiciera solo el sistema…", maxLength = 280, value, defaultValue = "", onChange, minRows = 3, maxRows = 8, className = "", style }: AreaTextoContadorProps) {
  const id = useId();
  const ref = useRef<HTMLTextAreaElement>(null);
  const [inner, setInner] = useState(defaultValue);
  const text = value ?? inner;
  const length = text.length;
  const ratio = length / maxLength;
  const state = ratio > 1 ? "over" : ratio > 0.85 ? "warn" : "ok";
  const remaining = maxLength - length;
  const circumference = 2 * Math.PI * 9;

  useLayoutEffect(() => {
    const el = ref.current;
    if (!el) return;
    const line = parseFloat(getComputedStyle(el).lineHeight) || 23;
    el.style.height = "auto";
    el.style.height = `${Math.min(Math.max(el.scrollHeight, line * minRows), line * maxRows)}px`;
  }, [text, minRows, maxRows]);

  return (
    <div className={`gcatc-root ${className}`} style={style} data-state={state}>
      <style>{CSS}</style>
      <label className="gcatc-label" htmlFor={id}>
        {label}
      </label>
      <textarea
        id={id}
        ref={ref}
        className="gcatc-area"
        rows={minRows}
        placeholder={placeholder}
        value={text}
        aria-describedby={`${id}-c`}
        onChange={(event) => {
          setInner(event.target.value);
          onChange?.(event.target.value);
        }}
      />
      <div className="gcatc-foot">
        <span id={`${id}-c`} className="gcatc-count" aria-live="polite">
          {remaining >= 0 ? `${remaining} caracteres disponibles` : `${-remaining} caracteres de más`}
        </span>
        <svg className="gcatc-ring" viewBox="0 0 24 24" aria-hidden="true">
          <circle className="t" cx="12" cy="12" r="9" />
          <circle className="v" cx="12" cy="12" r="9" strokeDasharray={circumference} strokeDashoffset={circumference * (1 - Math.min(1, ratio))} />
        </svg>
      </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/area-texto-contador

Ejemplo de uso de Área de texto con anillo contador

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

ejemplo.tsx5 líneas
import { AreaTextoContador } from "./area-texto-contador";

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

Propiedades de AreaTextoContador

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

PropiedadTipoDescripción
label?stringOpcional.
placeholder?stringOpcional.
maxLength?numberOpcional.
value?stringOpcional.
defaultValue?stringOpcional.
onChange?(value: string) => voidOpcional.
minRows?numberOpcional.
maxRows?numberOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Accesibilidad y rendimiento

El archivo tiene 88 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Área de texto con anillo contador 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-*.
  • 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)