Ir al contenido
Evaluar mi proyecto
Componentes · BotonesDisponible

Botón de copiar con confirmación

Comando seleccionable con copia real al portapapeles, confirmación y recuperación si el navegador bloquea la acción.

npm install mi-proyecto

Código completo

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

boton-copiar.tsx94 líneas
Descargar
"use client";

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

/** Botón de copiar con confirmación. React + TypeScript, sin dependencias. Personaliza con variables --gcxc-*. */
export type BotonCopiarProps = {
  text?: string;
  label?: string;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcxc-root { box-sizing: border-box; min-width: 0; max-width: 100%; color: var(--gcxc-fg, #202421); font: 400 15px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.gcxc-root *, .gcxc-root *::before, .gcxc-root *::after { box-sizing: border-box; }
.gcxc-root button, .gcxc-root input, .gcxc-root textarea, .gcxc-root select { font: inherit; }
.gcxc-root button, .gcxc-root a { -webkit-tap-highlight-color: transparent; }
.gcxc-root :focus-visible { outline: 2px solid var(--gcxc-accent, #25724b); outline-offset: 4px; }
.gcxc-root ::selection { background: var(--gcxc-accent, #25724b); color: #fff; }
.gcxc-root button:disabled { opacity: .5; cursor: not-allowed; }
.gcxc-root svg { flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) { .gcxc-root *, .gcxc-root *::before, .gcxc-root *::after { animation: none !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } }
.gcxc-root { position: relative; display: flex; align-items: center; gap: 20px; padding: 12px 14px 12px 22px; border: 1px solid #d0d7d2; border-radius: 12px; background: var(--gcxc-bg, #fff); flex-wrap: wrap; }
.gcxc-root code { font: 13px/1.5 ui-monospace, monospace; overflow-wrap: anywhere; flex: 1; }
.gcxc-root button { display: flex; align-items: center; gap: 8px; min-height: 44px; border: 0; border-radius: 8px; padding: 0 14px; color: #24533c; background: #e9f2ec; cursor: pointer; transition: background .2s; }
.gcxc-root button:hover { background: #d6e8dc; }
.gcxc-root button[data-done="true"] { background: #24533c; color: #fff; }
.gcxc-status { position: absolute; top: calc(100% + 10px); left: 0; font-size: 12px; max-width: 100%; }`;

export function BotonCopiar({
  text = "npm install mi-proyecto",
  label = "Copiar comando",
  className = "",
  style,
}: BotonCopiarProps) {
  const [state, setState] = useState<"idle" | "copied" | "error">("idle");
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
  useEffect(
    () => () => {
      if (timer.current) clearTimeout(timer.current);
    },
    [],
  );
  async function copy() {
    try {
      await navigator.clipboard.writeText(text);
      setState("copied");
    } catch {
      setState("error");
    }
    if (timer.current) clearTimeout(timer.current);
    timer.current = setTimeout(() => setState("idle"), 2400);
  }
  return (
    <div className={`gcxc-root ${className}`} style={style}>
      <style>{CSS}</style>
      <code>{text}</code>
      <button
        type="button"
        onClick={copy}
        aria-label={label}
        data-done={state === "copied"}
      >
        <svg
          width="18"
          height="18"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="1.7"
          aria-hidden="true"
        >
          {state === "copied" ? (
            <path d="m5 12 4 4 10-10" />
          ) : (
            <>
              <rect x="8" y="8" width="12" height="12" rx="2" />
              <path d="M16 8V4H4v12h4" />
            </>
          )}
        </svg>
        <span>{state === "copied" ? "Copiado" : "Copiar"}</span>
      </button>
      <span className="gcxc-status" role="status">
        {state === "error"
          ? "No se pudo copiar. Selecciona el texto y cópialo manualmente."
          : state === "copied"
            ? "Comando copiado al portapapeles."
            : ""}
      </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/boton-copiar

Ejemplo de uso de Botón de copiar con confirmación

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

ejemplo.tsx5 líneas
import { BotonCopiar } from "./boton-copiar";

export default function Ejemplo() {
  return <BotonCopiar text="Ejemplo" />;
}

Propiedades de BotonCopiar

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

PropiedadTipoDescripción
text?stringOpcional.
label?stringOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Personaliza Botón de copiar con confirmación 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
--gcxc-fg#202421
--gcxc-accent#25724b
--gcxc-bg#fff

Accesibilidad y rendimiento

El archivo tiene 94 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Botón de copiar con confirmación 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.
  • 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)