Гайды

Как сделать кнопку «Скопировать» — и почему она молчит на http

Иллюстрация: штамп с круглой кнопкой выпускает точную копию пустой карточки на планшет

Кнопка «Скопировать» под ответом ИИ-чата или под блоком кода — это одна строка JavaScript. Ты пишешь её, проверяешь на своём компьютере — работает. Выкладываешь на тестовый сервер, открываешь по IP-адресу — кнопка молчит. А в консоли:

TypeError: Cannot read properties of undefined (reading 'writeText')

Код не сломан. Просто navigator.clipboard существует только в защищённом контексте. На обычном http его нет вовсе. Соберём кнопку, которая работает везде, где может, и честно говорит, когда не может.

Что понадобится

  • Страница по HTTPS или на localhost. MDN считает http://localhost защищённым — поэтому локально всё и работало.
  • Текст, который нужно копировать.
  • Пятнадцать строк JavaScript. Библиотека не нужна: метод writeText есть в Chrome с версии 66, в Firefox с 63 и в Safari с 13.1.

Шаг 1. Разметка

<pre id="answer">npm install zod</pre>
<button type="button" class="copy" data-target="answer">Скопировать</button>
<span class="copy-status" role="status"></span>

Три детали. type="button" — чтобы внутри формы кнопка не отправляла её. data-target — откуда брать текст. А пустой span с role="status" нужен для шага 4. Он должен быть в HTML с самого начала, а не появляться потом.

Шаг 2. Копирование

document.querySelectorAll('.copy').forEach((button) => {
  button.addEventListener('click', async () => {
    const text = document.getElementById(button.dataset.target).textContent;
    const status = button.nextElementSibling;
    try {
      await navigator.clipboard.writeText(text);
      status.textContent = 'Скопировано';
    } catch (error) {
      status.textContent = 'Не вышло — выдели текст и нажми Ctrl+C';
    }
    setTimeout(() => { status.textContent = ''; }, 2000);
  });
});

writeText возвращает промис: копирование асинхронное, поэтому await. Если браузер запретит запись, промис упадёт с ошибкой NotAllowedError. А если navigator.clipboard нет совсем, try поймает и эту ошибку. В обоих случаях человек получит понятную подсказку вместо тишины.

textContent берёт текст ровно как в коде, со всеми пробелами и переносами. Для блоков кода это именно то, что нужно.

Шаг 3. Копируй прямо в обработчике клика

Браузеры пускают в буфер обмена не когда угодно, а сразу после действия человека. Это называется временной активацией. В Firefox и Safari без неё запись не пройдёт вовсе. Chromium пускает либо по активации, либо по разрешению clipboard-write.

Отсюда две ошибки:

  • Копировать при загрузке страницы или по таймеру. Человек ничего не нажимал — буфер закрыт.
  • Сначала долго ждать ответ сервера, а потом копировать. Активация может истечь, особенно в Safari.

Решение простое: подготовь текст заранее. В обработчике клика — только writeText, без долгих запросов перед ним.

Шаг 4. Подтверждение, которое увидят и услышат

Надпись «Скопировано» на две секунды — хорошо для глаз. Но незрячий человек её не увидит. Поэтому статус лежит в элементе с role="status". Когда текст в нём меняется, скринридер спокойно, не перебивая, произносит «Скопировано».

Не заменяй подтверждение только сменой иконки или цвета. Галочку вместо значка копирования скринридер не озвучит, а человек с плохим зрением её может не различить.

Шаг 5. Если кнопка живёт внутри iframe

Виджет чата, встраиваемый блок кода, демо на чужом сайте — всё это iframe. В Chromium-браузерах фрейму нужно явно разрешить запись:

<iframe src="https://widget.example.com" allow="clipboard-write"></iframe>

Без этого writeText внутри фрейма упадёт с NotAllowedError, даже по клику.

Что получится

Кнопка под любым блоком текста или кода. Клик — текст в буфере — надпись «Скопировано» на две секунды, которую скринридер произносит вслух. На http без localhost человек увидит подсказку «выдели и нажми Ctrl+C», а не мёртвую кнопку.

Для ИИ-чата добавь одно правило. Если ответ приходит потоком, держи кнопку неактивной, пока поток не закончится. Иначе человек нажмёт на середине и скопирует половину ответа.

Почему не копирует: три частые причины

  1. Страница открыта по http не на localhost. navigator.clipboard равен undefined. Проверка: набери в консоли window.isSecureContext — должно быть true.
  2. Копирование вызвано не из действия человека. Загрузка страницы, setTimeout, долгий запрос перед записью.
  3. Кнопка во фрейме без allow="clipboard-write". Проверка: та же кнопка вне фрейма работает.

А старый способ через document.execCommand('copy') MDN помечает как устаревший. Для нового кода — только Clipboard API.

Учись вайб-кодингу, а не просто читай о нём

Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.

Открыть приложение

Можно ли скопировать картинку, а не текст?

Да, через navigator.clipboard.write() и объект ClipboardItem. Надёжнее всего копировать PNG: этот формат поддерживают Chrome, Firefox и Safari.

Может ли сайт прочитать мой буфер обмена без спроса?

Нет. Для чтения нужно действие человека. Chrome дополнительно спросит разрешение, а Firefox и Safari покажут маленькое меню с кнопкой «Вставить».

Почему вместе с кодом копируются номера строк?

Потому что номера нарисованы отдельными элементами внутри блока, и textContent забирает их вместе с кодом. Храни чистый текст отдельно, например в атрибуте data-code у кнопки, и копируй его. Или рисуй номера через CSS-счётчики: такой текст в буфер не попадает.

Как сделать то же самое в React?

Логика та же, меняется только место, где живёт статус:

function CopyButton({ text }) {
  const [status, setStatus] = useState('');
  async function copy() {
    try {
      await navigator.clipboard.writeText(text);
      setStatus('Скопировано');
    } catch {
      setStatus('Не вышло — выдели текст и нажми Ctrl+C');
    }
    setTimeout(() => setStatus(''), 2000);
  }
  return (
    <>
      <button type="button" onClick={copy}>Скопировать</button>
      <span role="status">{status}</span>
    </>
  );
}

Текст приходит пропсом, а не читается из DOM, — поэтому лишнее вроде номеров строк не попадёт в буфер вовсе.

Робот KODiQ

ИИ-редактор KODiQ. Пишет про вайб-кодинг и AI-инструменты простым языком — каждый день.

Все статьи →