Kalian akan menambahkan tombol copy pada code block dengan JavaScript di sisi klien, memanfaatkan data attributes dan struktur figure, menyiapkan layout responsive, serta memastikan aksesibilitas lewat aria-label dan navigasi keyboard.

Highlight membuat kode terbaca, tetapi pembaca tetap perlu menyalinnya untuk dicoba. Menyalin manual baris demi baris menjengkelkan, terutama untuk blok panjang. Sebuah tombol copy yang rapi meningkatkan pengalaman nyata dari dokumentasi.
Episode 16 membahas copy button dan interaksi UI: cara menemukan code block lewat data attributes, menambahkan tombol dengan JavaScript di sisi klien, membuat layout responsive, dan memenuhi standar aksesibilitas seperti aria-label dan navigasi keyboard.
Setiap code block hasil render berbentuk <figure> yang berisi <pre> dan <code> di dalamnya. Elemen figure membawa atribut data-rehype-pretty-code-figure, sedangkan baris dan token memakai atribut data-* lain. Struktur inilah yang menjadi target JavaScript untuk menyisipkan tombol.
Karena markup selalu konsisten, kalian bisa menulis satu skrip yang bekerja untuk semua blok. Cukup pilih semua elemen dengan atribut data-rehype-pretty-code-figure, lalu untuk masing-masing elemen tambahkan tombol di lokasi yang diinginkan.
Teks yang disalin diambil dari elemen <code>, bukan dari teks tampilan yang sudah diwarnai. Properti textContent mengembalikan teks bersih tanpa tag span:
const code = pre.querySelector("code");
const text = code.textContent.trim();Perlu diperhatikan, textContent juga menyertakan teks dari anotasi seperti // + pada blok diff. Filter baris anotasi sebelum disalin jika kalian ingin hasil copy bersih dari marker.
Pada Next.js App Router, buat komponen klien yang menjalankan logika penyisipan setelah halaman dirender:
"use client";
import { useEffect } from "react";
export function CopyButtons() {
useEffect(() => {
document
.querySelectorAll("[data-rehype-pretty-code-figure]")
.forEach((figure) => {
if (figure.querySelector("[data-copy-button]")) return;
const button = document.createElement("button");
button.dataset.copyButton = "true";
button.textContent = "Salin";
button.addEventListener("click", async () => {
const code = figure.querySelector("code");
await navigator.clipboard.writeText(code.textContent);
button.textContent = "Tersalin";
});
figure.appendChild(button);
});
}, []);
return null;
}Komponen ini menambahkan satu tombol per blok dan tidak duplikat berkat pengecekan data-copy-button. Setelah menyalin, teks tombol berubah menjadi "Tersalin" sebagai umpan balik singkat.
Pada halaman yang dirender ulang secara dinamis, useEffect bisa berjalan lebih dari sekali. Pengecekan querySelector("[data-copy-button]") mencegah tombol ganda. Alternatifnya, hapus tombol lama sebelum membuat yang baru agar state tetap bersih.
Tombol diletakkan di pojok atas blok, sejajar dengan title. Karena figure belum tentu memiliki position, pastikan CSS menyediakan positioning yang konsisten:
[data-rehype-pretty-code-figure] {
position: relative;
}
[data-rehype-pretty-code-figure] [data-copy-button] {
position: absolute;
top: 0.6rem;
right: 0.6rem;
}Dengan posisi absolut di dalam figure, tombol tidak menggeser tata letak kode dan tetap konsisten di semua blok.
Pada ponsel, tombol tidak boleh menutupi baris pertama. Sembunyikan title atau kecilkan tombol pada layar sempit, dan pastikan area sentuh minimal 44 piksel:
@media (max-width: 640px) {
[data-rehype-pretty-code-figure] [data-copy-button] {
top: 0.4rem;
right: 0.4rem;
min-height: 44px;
}
}Area sentuh yang cukup dan posisi yang tidak menimpa kode membuat pengalaman mobile tetap nyaman.
Tombol copy hanya berisi teks "Salin" yang tidak menjelaskan objeknya. Tambahkan aria-label yang merujuk bahasa blok, misalnya "Salin kode TypeScript". Dengan begitu screen reader memberi konteks penuh:
const lang = pre.dataset.lang || "kode";
button.setAttribute("aria-label", `Salin ${lang}`);Tidak semua bahasa tersedia di atribut. Gunakan fallback kata "kode" saat bahasa tidak diketahui.
Tombol adalah elemen <button> asli sehingga otomatis dapat difokus dengan tombol Tab dan diaktifkan dengan Enter. Pastikan outline fokus tetap terlihat saat tombol menerima fokus, karena pengguna keyboard bergantung pada indikator tersebut. Jangan menonaktifkan outline kecuali menyediakan pengganti yang jelas.
Umpan balik "Tersalin" harus dikembalikan ke keadaan semula agar pengguna tidak bingung. Reset teks tombol setelah beberapa detik, dan berikan status lewat aria-live supaya screen reader mengumumkannya. Umpan balik singkat seperti ini membuat interaksi terasa selesai.
Inti yang harus dibawa pulang:
data-rehype-pretty-code-figure.textContent elemen <code>.aria-label memberi konteks bahasa pada screen reader.Di episode 17 kalian akan belajar custom HAST dan rehype plugin lanjutan: memodifikasi node figure dan pre setelah rehype-pretty-code, menyusun rangkaian plugin yang benar, serta memahami urutan transformasi dalam pipeline yang kompleks.