Tema
Copiar al portapapeles
Botones de "Copiar" para códigos de descuento, números de cuenta (CLABE), enlaces para compartir o bloques de código. Usa la API moderna navigator.clipboard con aviso visual y un respaldo para navegadores antiguos o páginas sin HTTPS.
Función reutilizable
js
/**
* Copia texto al portapapeles.
* @returns {Promise<boolean>} true si se copió
*/
export async function copiar(texto) {
// API moderna: requiere HTTPS (o localhost) y que se llame desde un clic
if (navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(texto);
return true;
} catch {
// Si el usuario negó el permiso, intentamos el método clásico
}
}
// Respaldo: textarea temporal + execCommand (obsoleto pero aún funciona)
const area = document.createElement('textarea');
area.value = texto;
area.setAttribute('readonly', '');
area.style.cssText = 'position:fixed;top:-9999px;opacity:0';
document.body.appendChild(area);
area.select();
area.setSelectionRange(0, texto.length); // iOS
let ok = false;
try { ok = document.execCommand('copy'); } catch { ok = false; }
area.remove();
return ok;
}
/** Aviso flotante breve ("toast") */
export function aviso(mensaje, tipo = 'ok') {
const el = document.createElement('div');
el.textContent = mensaje;
el.setAttribute('role', 'status');
el.style.cssText = `position:fixed;left:50%;bottom:24px;transform:translateX(-50%) translateY(20px);
background:${tipo === 'ok' ? '#16a34a' : '#dc2626'};color:#fff;padding:10px 18px;border-radius:999px;
font:600 14px system-ui;box-shadow:0 8px 24px rgb(0 0 0 / .2);opacity:0;transition:.25s;z-index:9999`;
document.body.appendChild(el);
requestAnimationFrame(() => { el.style.opacity = '1'; el.style.transform = 'translateX(-50%)'; });
setTimeout(() => { el.style.opacity = '0'; setTimeout(() => el.remove(), 300); }, 1800);
}
/**
* Activa todos los botones con data-copiar.
* data-copiar="texto literal" → copia ese texto
* data-copiar-de="#selector" → copia el texto (o value) de otro elemento
*/
export function activarBotonesCopiar(raiz = document) {
raiz.addEventListener('click', async (e) => {
const btn = e.target.closest('[data-copiar], [data-copiar-de]');
if (!btn) return;
const origen = btn.dataset.copiarDe && document.querySelector(btn.dataset.copiarDe);
const texto = btn.dataset.copiar ?? (origen ? (origen.value ?? origen.textContent) : '');
const ok = await copiar(texto.trim());
aviso(ok ? '¡Copiado!' : 'No se pudo copiar', ok ? 'ok' : 'error');
if (ok) {
const original = btn.innerHTML;
btn.textContent = '✓ Copiado';
btn.disabled = true;
setTimeout(() => { btn.innerHTML = original; btn.disabled = false; }, 1500);
}
});
}HTML de ejemplo
html
<!-- Cupón -->
<div class="cupon">
<code id="cupon">BIENVENIDA10</code>
<button data-copiar-de="#cupon">Copiar código</button>
</div>
<!-- Datos bancarios -->
<p>CLABE: <span id="clabe">012180001234567891</span>
<button data-copiar-de="#clabe" aria-label="Copiar CLABE">📋</button></p>
<!-- Texto fijo -->
<button data-copiar="https://minegocio.mx/promo">Copiar enlace de la promo</button>
<!-- Campo de formulario -->
<input id="link" value="https://minegocio.mx/ref/ana23" readonly>
<button data-copiar-de="#link">Copiar</button>
<script type="module">
import { activarBotonesCopiar } from '/js/copiar.js';
activarBotonesCopiar();
</script>Botón "Copiar" para todos los bloques de código
js
document.querySelectorAll('pre > code').forEach((code) => {
const btn = document.createElement('button');
btn.textContent = 'Copiar';
btn.className = 'btn-copiar-codigo';
btn.addEventListener('click', async () => {
const ok = await copiar(code.innerText);
btn.textContent = ok ? '¡Copiado!' : 'Error';
setTimeout(() => (btn.textContent = 'Copiar'), 1500);
});
code.parentElement.style.position = 'relative';
code.parentElement.appendChild(btn);
});css
.btn-copiar-codigo {
position: absolute; top: 8px; right: 8px;
padding: 4px 10px; font-size: 12px; border-radius: 6px;
border: 1px solid #475569; background: #1e293b; color: #e2e8f0; cursor: pointer;
}Compartir con la API nativa (móviles)
En celulares es aún mejor abrir el menú de compartir del sistema (WhatsApp, Messenger, etc.) y usar copiar como respaldo:
js
async function compartir({ titulo, texto, url }) {
if (navigator.share) {
try {
await navigator.share({ title: titulo, text: texto, url });
return;
} catch (err) {
if (err.name === 'AbortError') return; // el usuario cerró el menú
}
}
const ok = await copiar(url);
aviso(ok ? 'Enlace copiado' : 'No se pudo copiar', ok ? 'ok' : 'error');
}
document.querySelector('#btn-compartir').addEventListener('click', () =>
compartir({ titulo: 'Café de Chiapas', texto: '¡Mira este café!', url: location.href }));Error común
Llamar a navigator.clipboard.writeText() fuera de un evento de usuario (por ejemplo, en un setTimeout largo o al cargar la página). Los navegadores lo bloquean por seguridad: sólo funciona como respuesta directa a un clic o una tecla.
HTTPS
navigator.clipboard sólo existe en contextos seguros (HTTPS o localhost). Si abres tu archivo con file:// o en un hosting sin SSL, se usa el respaldo con execCommand.