Want a useful JavaScript copy to clipboard example that goes beyond copying “Hello world”? Let’s build a small kaomoji picker. Click a face, copy the complete expression, and paste it into a message, document, or comment. No framework, icon library, or build tools required.
Kaomoji are text faces such as (^▽^) and ʕ•ᴥ•ʔ. They make a good practice project because the interface is simple, while the strings introduce a few real-world details: Unicode characters, backslashes, browser permissions, and accessible feedback.
The finished picker has six buttons, a visible focus outline, and a status message. When automatic copying is unavailable, it offers a selected text field for manual copying. The same pattern works for coupon codes, command-line snippets, and invitation links.
For a ready-made option alongside this project, see the kaomoji generator on Coding Dude. Here, we’ll focus on building the copying behavior ourselves.
Interface illustration: each button copies one complete face; the focus outline and status area make the interaction easier to use.
JavaScript copy to clipboard: the smallest working example
The method we need is navigator.clipboard.writeText(). Give it a string and wait for its promise to resolve before telling the user the copy succeeded. Here is a complete starter example:
<button id="copy-happy" type="button">Copy happy face</button> <p id="message" role="status"></p> <script> const message = document.querySelector("#message"); document.querySelector("#copy-happy").addEventListener("click", async () => { message.textContent = "Copying…"; try { await navigator.clipboard.writeText("(^▽^)"); message.textContent = "Copied!"; } catch { message.textContent = "Copy failed. Select and copy this face: (^▽^)"; } }); </script> Save this example in an HTML file and serve it from HTTPS or a local development server. The click handler provides the user action browsers expect for copying. A failed operation goes to catch, so the button never reports success simply because it was clicked.
For the full project, we’ll separate the markup, styling, and behavior. Put the HTML inside the body, the CSS inside a style element in the head, and the JavaScript in a script element just before the closing body tag.
1. Add the HTML structure
The picker needs a container for its buttons, a status region, and a manual-copy area. JavaScript will create the six buttons from an array, so you only need to maintain the list in one place.
<main class="picker" aria-labelledby="picker-title"> <p class="eyebrow">VANILLA JAVASCRIPT · SMALL PROJECT</p> <h1 id="picker-title">Kaomoji picker</h1> <p id="picker-help">Choose a face to copy it. Then paste it into your message.</p> <div id="faces" class="faces" aria-describedby="picker-help"></div> <p id="copy-status" class="status" role="status" aria-live="polite" aria-atomic="true">Ready when you are.</p> <div id="manual-copy" class="manual" hidden> <label for="copy-text">Text to copy manually</label> <p id="manual-help">Press Ctrl+C on Windows/Linux or Command+C on Mac. On mobile, touch and hold the selected text and choose Copy.</p> <textarea id="copy-text" rows="2" readonly aria-describedby="manual-help" spellcheck="false"></textarea> </div> <noscript><p>Enable JavaScript to use the picker. You can still select and copy this face manually: (^▽^)</p></noscript> </main> The status region exists before any interaction happens. Its role="status" and polite live announcement let assistive technology announce updates without moving focus after every successful copy. The manual-copy field starts hidden and has a visible label plus platform-specific instructions.
A real button already supports mouse clicks, touch, Enter, and Space. We don’t need to recreate that behavior with a clickable div. The noscript message also leaves a face available to select when JavaScript is turned off.
2. Style a responsive picker with CSS
CSS Grid gives us flexible columns without a long list of breakpoints. Each button has enough room for its text face and label. On narrow screens, the grid drops to fewer columns rather than squeezing the expressions into unreadable fragments.
* { box-sizing: border-box; } body { margin: 0; min-height: 100vh; padding: clamp(1rem, 5vw, 4rem); color: #15283b; background: #f7f4ed; font: 1rem/1.6 system-ui, sans-serif; } .picker { max-width: 52rem; margin: auto; padding: clamp(1rem, 4vw, 2.5rem); border: 1px solid #ced8d5; border-radius: 1.25rem; background: #fff; } .eyebrow { font-size: .75rem; font-weight: 700; letter-spacing: .08em; } h1 { margin: .25rem 0; font-size: clamp(1.8rem, 5vw, 2.75rem); line-height: 1.2; } .faces { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); gap: .75rem; margin: 1.5rem 0; } .face-button { min-width: 0; min-height: 6rem; padding: .8rem; border: 1px solid #b7cbc7; border-radius: .75rem; background: #f2f8f6; color: #15283b; font: inherit; cursor: pointer; } .face-button:hover { background: #dcefe9; } .face-button:focus-visible, textarea:focus-visible { outline: 3px solid #087c72; outline-offset: 4px; } .face { display: block; font-size: 1.15rem; white-space: pre; overflow-x: auto; } .face-label { display: block; margin-top: .4rem; font-size: .85rem; font-weight: 650; } .status { padding: .75rem 1rem; border-radius: .5rem; background: #e4f3ee; } .manual { padding: 1rem; border: 1px solid #a16b16; border-radius: .75rem; } .manual label { font-weight: 700; } .manual p { font-size: .9rem; } textarea { width: 100%; padding: .75rem; font: 1.1rem/1.5 system-ui, sans-serif; color: #15283b; background: #fff; resize: vertical; } The focus outline is deliberately easy to see. Try navigating with Tab before adding more visual effects. A hover color helps mouse users, but it cannot replace keyboard focus.
The face uses white-space: pre so intentional spaces stay visible. Very long expressions can scroll inside their button instead of stretching the whole page. You can change the palette to match your website while keeping text contrast and the focus outline clear.
3. Copy complete strings with JavaScript
Each item has a human-readable label and the exact string to copy. The label gives the button a useful accessible name; the decorative face is hidden from screen readers to avoid announcing a long sequence of punctuation.
The copy operation has two outcomes: announce success after it resolves, or offer selectable text when automatic copying fails.
const kaomojis = [ { label: "Happy", text: "(^▽^)" }, { label: "Crying", text: "(T_T)" }, { label: "Shrug", text: "¯\\_(ツ)_/¯" }, { label: "Hug", text: "(づ。◕‿‿◕。)づ" }, { label: "Bear", text: "ʕ•ᴥ•ʔ" }, { label: "Table flip", text: "(╯°□°)╯︵ ┻━┻" } ]; const faces = document.querySelector("#faces"); const status = document.querySelector("#copy-status"); const manual = document.querySelector("#manual-copy"); const copyText = document.querySelector("#copy-text"); let copying = false; async function copyFace(face) { if (copying) return; copying = true; faces.setAttribute("aria-busy", "true"); manual.hidden = true; status.textContent = `Copying: ${face.label}…`; try { if (!window.isSecureContext || !navigator.clipboard?.writeText) { throw new Error("Clipboard API unavailable"); } await navigator.clipboard.writeText(face.text); status.textContent = `Copied: ${face.label}. Paste it wherever you like.`; } catch { copyText.value = face.text; manual.hidden = false; status.textContent = "Automatic copy is unavailable. Select the text below and copy it manually."; copyText.focus(); copyText.select(); copyText.setSelectionRange(0, copyText.value.length); } finally { copying = false; faces.removeAttribute("aria-busy"); } } for (const face of kaomojis) { const button = document.createElement("button"); button.type = "button"; button.className = "face-button"; button.setAttribute("aria-label", `Copy ${face.label} kaomoji`); const symbol = document.createElement("span"); symbol.className = "face"; symbol.setAttribute("aria-hidden", "true"); symbol.textContent = face.text; const label = document.createElement("span"); label.className = "face-label"; label.textContent = face.label; button.append(symbol, label); button.addEventListener("click", () => copyFace(face)); faces.append(button); } The copying flag ignores extra activations while a request is pending. That prevents two overlapping requests from racing to update the status. Buttons keep their focus, and they work again when the request finishes.
Notice that we use textContent to display each face. A face is text, so there’s no reason to interpret it as HTML. This also preserves literal characters such as angle brackets when you expand the collection.
If the API is missing or rejects the request, the code reveals the read-only textarea, puts the original string inside it, and selects it. Moving focus is useful here because there’s a new task to complete manually. Selection menus differ on mobile, so the instructions explain touch-and-hold as well.
4. Customize the faces without breaking the strings
To customize the picker, choose a few expressions from this kaomoji collection and add them to your JavaScript array. Keep each face as one complete string so its characters stay together.
You can change the label independently of the face. For example, a team chat might use “Celebration” or “Need coffee.” Keep labels descriptive enough that someone can choose a button without seeing its symbol.
Keep each face as one string. In an ordinary JavaScript string literal, two backslashes represent one literal backslash.
Backslashes deserve special attention. In an ordinary quoted JavaScript string literal, a backslash starts an escape sequence. To put one literal backslash in the resulting text, write two in the source:
const shrug = "¯\\_(ツ)_/¯"; console.log(shrug); // ¯\_(ツ)_/¯ That doubling belongs in JavaScript source. When you paste a face into an HTML text node or type it into a text field, you don’t apply JavaScript string escaping. Likewise, choose quotation marks carefully if a new face contains an apostrophe or a double quote.
Keep the string intact instead of splitting it into letters. Some Unicode characters use multiple code units, and some visible symbols combine several characters. This picker avoids that problem by passing the original string directly to the clipboard.
Why does copying fail on some pages?
The Clipboard API requires a secure context. Use HTTPS on your live site; localhost is generally treated as trustworthy for development. Don’t rely on opening a downloaded file directly to test every browser’s clipboard behavior.
Copying can also fail because of browser policy, document focus, or restrictions on an embedded demo. Keep the request directly inside the user-triggered handler, and avoid an unrelated delay before it. If an embedded version fails, open the standalone demo in its own tab.
The writeText documentation explains the method’s promise and secure-context requirement. Our fallback makes failure recoverable without trying to read clipboard contents or requiring an extra library.
Reuse the copy button in your own projects
To turn this into a snippet picker, replace each face with a command or short code sample and update its label. The copying function doesn’t depend on kaomoji: it receives the selected item and copies its text. For multiline snippets, use a JavaScript template literal and give the manual textarea more rows so readers can inspect the content.
Keep the page’s character encoding set to UTF-8, as it is in the complete download. If a face appears as empty boxes on another device, check the font’s character coverage before changing the string. A font can change how a symbol looks without changing what gets copied.
This example writes only the selected text. It doesn’t read the user’s existing clipboard, store a copy history, or send the expressions to a server. That keeps the small project easy to understand and makes the copy function straightforward to reuse.
Assemble and test the complete demo
The HTML, CSS, and JavaScript sections above contain the complete picker. Create an HTML document with UTF-8 encoding and a viewport meta tag. Put the CSS inside a style element in the head, the picker markup inside the body, and the JavaScript inside a script element just before the closing body tag.
Save the file as kaomoji-picker-demo.html, serve it from your local development server, and open the page in your browser. Nothing needs compiling.
Before reusing it, copy each face into a plain-text editor and check the result. Try Tab and Enter, then Space. Finally, test the manual-copy path with clipboard access blocked. Those small checks are more useful than testing only the happy-face button once.
Want another project with expressive text? Try my JavaScript emoji mouse trail tutorial. For a different treatment, experiment with the wavy text generator. Keep the clipboard picker’s faces intact even when you explore effects that animate individual characters.
You now have a reusable copy-button pattern: store the original text, wait for the clipboard operation, give clear feedback, and offer a manual way forward when copying is blocked.



