Share Button
Press Share and the options slide out from behind it, the last one pushing the pill open, while the arrow takes off and a cross draws itself. Copy link turns into a check; X and LinkedIn open in a small window.
View codeA command, or a sentence for your AI, with a copy button: the icon turns into a check that draws itself and the label rolls to "Copied".
With your AI
Say it to your AI. It gets the exact code below, not its best guess, and knows where it goes.
Add Kodza Copy to Clipboard to my install command.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<div class="kodza-copy" data-kodza-copy>
<code class="kodza-copy__code">git commit -m "works on my machine"</code>
<button class="kodza-copy__button" type="button">
<span class="kodza-copy__icon" aria-hidden="true">
<svg class="kodza-copy__glyph kodza-copy__glyph--copy" viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5" /><path d="M10.5 3.5v-.5a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h.5" /></svg>
<svg class="kodza-copy__glyph kodza-copy__glyph--done" viewBox="0 0 16 16"><path d="M3 8.5l3.2 3L13 4.5" pathLength="1" /></svg>
</span>
<span class="kodza-copy__label">
<span class="kodza-copy__word">Copy</span>
<span class="kodza-copy__word kodza-copy__word--done" aria-hidden="true">Copied</span>
</span>
</button>
<span class="kodza-copy__status" role="status"></span>
</div>
<div class="kodza-copy kodza-copy--prompt" data-kodza-copy>
<p class="kodza-copy__code">Add the Kodza fill button to my hero, and make it say "Ship it".</p>
<button class="kodza-copy__button" type="button">
<span class="kodza-copy__icon" aria-hidden="true">
<svg class="kodza-copy__glyph kodza-copy__glyph--copy" viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5" /><path d="M10.5 3.5v-.5a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h.5" /></svg>
<svg class="kodza-copy__glyph kodza-copy__glyph--done" viewBox="0 0 16 16"><path d="M3 8.5l3.2 3L13 4.5" pathLength="1" /></svg>
</span>
<span class="kodza-copy__label">
<span class="kodza-copy__word">Copy</span>
<span class="kodza-copy__word kodza-copy__word--done" aria-hidden="true">Copied</span>
</span>
</button>
<span class="kodza-copy__status" role="status"></span>
</div>
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Copy to Clipboard
A command and its copy button. Once copied, the icon turns into a check that draws itself
and the label rolls to "Copied", then everything quietly comes back. --prompt makes it a
sentence to paste into an AI: bigger, in the text font, wrapping instead of scrolling.
Everything is tuned with the --kz-copy-* variables. */
/* Fills its column at zero weight: a theme that sizes its content blocks (Blocksy puts the page
gutter in each block's width) keeps the last word */
:where(.kodza-copy) {
width: 100%;
}
.kodza-copy {
--kz-copy-bg: #ffffff;
--kz-copy-fg: #0a0a0a;
--kz-copy-muted: rgba(10, 10, 10, 0.45);
--kz-copy-border: rgba(10, 10, 10, 0.1);
--kz-copy-button-bg: #0a0a0a;
--kz-copy-button-fg: #ffffff;
--kz-copy-radius: 14px;
--kz-copy-font: ui-monospace, "SF Mono", "Geist Mono", Menlo, monospace;
--kz-copy-size: 14px;
--kz-copy-prompt: "$ ";
--kz-copy-button-height: 38px;
--kz-copy-max-width: 520px;
--kz-copy-duration: 0.45s;
--kz-copy-ease: cubic-bezier(0.22, 1, 0.36, 1);
/* The command takes what is left and scrolls: it never widens its column (a 1fr grid track,
a phone), yet a box that shrinks to fit still gets the whole line */
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 12px;
max-width: var(--kz-copy-max-width);
padding: 8px 8px 8px 18px;
border-radius: var(--kz-copy-radius);
background: var(--kz-copy-bg);
box-shadow: inset 0 0 0 1px var(--kz-copy-border);
color: var(--kz-copy-fg);
box-sizing: border-box;
}
/* Long commands scroll sideways instead of breaking */
.kodza-copy__code {
min-width: 0;
margin: 0;
overflow-x: auto;
font-family: var(--kz-copy-font);
font-size: var(--kz-copy-size);
line-height: 1.5;
white-space: nowrap;
scrollbar-width: none;
}
.kodza-copy__code::-webkit-scrollbar {
display: none;
}
/* The prompt is drawn, so it is never part of what gets copied */
.kodza-copy__code::before {
content: var(--kz-copy-prompt);
color: var(--kz-copy-muted);
user-select: none;
}
.kodza-copy__button {
display: inline-flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
height: var(--kz-copy-button-height);
padding: 0 14px 0 12px;
border: 0;
border-radius: calc(var(--kz-copy-radius) - 6px);
background: var(--kz-copy-button-bg);
color: var(--kz-copy-button-fg);
font: inherit;
font-size: 14px;
font-weight: 500;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
transition: transform 0.2s var(--kz-copy-ease), opacity 0.2s ease;
}
.kodza-copy__button:hover {
opacity: 0.88;
}
.kodza-copy__button:active {
transform: scale(0.95);
}
.kodza-copy__button:focus-visible {
outline: 2px solid var(--kz-copy-button-bg);
outline-offset: 3px;
}
/* Two icons in the same spot: the copy icon steps back, the check draws itself */
.kodza-copy__icon {
display: grid;
width: 16px;
height: 16px;
}
.kodza-copy__glyph {
grid-area: 1 / 1;
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform var(--kz-copy-duration) var(--kz-copy-ease), opacity calc(var(--kz-copy-duration) * 0.6) ease;
}
.kodza-copy__glyph--done {
stroke-width: 1.8;
stroke-dasharray: 1;
stroke-dashoffset: 1;
transition: stroke-dashoffset var(--kz-copy-duration) var(--kz-copy-ease);
}
.kodza-copy.is-copied .kodza-copy__glyph--copy {
opacity: 0;
transform: scale(0.5) rotate(-20deg);
}
.kodza-copy.is-copied .kodza-copy__glyph--done {
stroke-dashoffset: 0;
transition-delay: calc(var(--kz-copy-duration) * 0.25);
}
/* Both words share one cell, so the button keeps the width of the longest */
.kodza-copy__label {
display: grid;
overflow: hidden;
line-height: 1.3;
}
.kodza-copy__word {
grid-area: 1 / 1;
transition: transform var(--kz-copy-duration) var(--kz-copy-ease);
}
.kodza-copy__word--done {
transform: translateY(110%);
}
.kodza-copy.is-copied .kodza-copy__word {
transform: translateY(-110%);
}
.kodza-copy.is-copied .kodza-copy__word--done {
transform: none;
}
/* --prompt: a sentence for an AI. It wraps (the button drops under it when the line runs
out of room) and the prompt is a chevron */
.kodza-copy--prompt {
--kz-copy-size: clamp(17px, 1.6vw, 21px);
--kz-copy-prompt: "› ";
--kz-copy-button-height: 48px;
--kz-copy-radius: 22px;
--kz-copy-max-width: 760px;
display: flex;
flex-wrap: wrap;
gap: 12px 16px;
padding: 10px 10px 10px 24px;
}
.kodza-copy--prompt .kodza-copy__code {
flex: 1 1 260px;
overflow: visible;
font-family: inherit;
font-weight: 500;
line-height: 1.35;
letter-spacing: -0.02em;
white-space: normal;
text-wrap: pretty;
}
.kodza-copy--prompt .kodza-copy__button {
margin-left: auto;
padding: 0 20px 0 18px;
font-size: 15px;
}
.kodza-copy__status {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
@media (prefers-reduced-motion: reduce) {
.kodza-copy__button,
.kodza-copy__glyph,
.kodza-copy__word {
transition-duration: 0.01ms !important;
transition-delay: 0s !important;
}
}
/* Theming example — a dark terminal:
.dark-section .kodza-copy {
--kz-copy-bg: #161616;
--kz-copy-fg: #f2f2f0;
--kz-copy-muted: rgba(255, 255, 255, 0.4);
--kz-copy-border: rgba(255, 255, 255, 0.1);
--kz-copy-button-bg: #ffffff;
--kz-copy-button-fg: #0a0a0a;
}
*/
/* kodza-guard f4dd58651d71 — generated by Kodza, do not edit.
Theme guard: keeps a site's own CSS (WordPress themes, Bootstrap, CSS resets…) off this
component. :not(#_) is always true and weighs as much as an id, so these lines win over
theme rules made of classes and tags. Customise the component through its --kz-* variables. */
/* What the browser gives, where the component sets it only in some states: at the lowest weight, so its own rules below put it back */
:where(.kodza-copy__code):not(#_) {
font-weight: revert;
letter-spacing: revert;
}
:where(.kodza-copy__button):not(#_) {
outline: revert;
margin-left: revert;
outline-offset: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-copy__code:not(#_) {
border: revert;
padding: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
transition: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
background-color: revert;
background-image: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: revert;
color: revert;
font-style: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-copy__button:not(#_) {
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
margin-top: revert;
margin-right: revert;
margin-bottom: revert;
box-shadow: revert;
appearance: revert;
max-width: revert;
max-height: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
white-space: revert;
min-height: revert;
}
.kodza-copy__icon:not(#_),
.kodza-copy__label:not(#_),
.kodza-copy__word:not(#_),
.kodza-copy__status:not(#_) {
box-sizing: revert;
}
.kodza-copy__glyph:not(#_) {
border: revert;
margin: revert;
padding: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
background-color: revert;
background-image: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: revert;
color: revert;
font-family: revert;
font-size: revert;
font-weight: revert;
font-style: revert;
line-height: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-copy__code:not(#_):where(:not([hidden])),
.kodza-copy__glyph:not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-copy__code:not(#_):where([hidden]),
.kodza-copy__glyph:not(#_):where([hidden]) {
display: none;
}
.kodza-copy__code:not(#_)::before {
box-sizing: revert;
}
/* The component's own rules, one step stronger: real buttons and fields, what it sets on tags themes style (links, lists, headings, images…), and the root width (a theme may narrow it, never widen it) */
.kodza-copy:not(#_) {
max-width: var(--kz-copy-max-width);
}
.kodza-copy__code:not(#_) {
margin: 0;
font-family: var(--kz-copy-font);
font-size: var(--kz-copy-size);
line-height: 1.5;
}
.kodza-copy__button:not(#_) {
display: inline-flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
height: var(--kz-copy-button-height);
padding: 0 14px 0 12px;
border: 0;
border-radius: calc(var(--kz-copy-radius) - 6px);
background: var(--kz-copy-button-bg);
color: var(--kz-copy-button-fg);
font: inherit;
font-size: 14px;
font-weight: 500;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
transition: transform 0.2s var(--kz-copy-ease), opacity 0.2s ease;
}
.kodza-copy__button:hover:not(#_) {
opacity: 0.88;
}
.kodza-copy__button:active:not(#_) {
transform: scale(0.95);
}
.kodza-copy__button:focus-visible:not(#_) {
outline: 2px solid var(--kz-copy-button-bg);
outline-offset: 3px;
}
.kodza-copy__glyph:not(#_) {
transition: transform var(--kz-copy-duration) var(--kz-copy-ease), opacity calc(var(--kz-copy-duration) * 0.6) ease;
}
.kodza-copy__glyph--done:not(#_) {
transition: stroke-dashoffset var(--kz-copy-duration) var(--kz-copy-ease);
}
.kodza-copy.is-copied .kodza-copy__glyph--done:not(#_) {
transition-delay: calc(var(--kz-copy-duration) * 0.25);
}
.kodza-copy--prompt .kodza-copy__code:not(#_) {
font-family: inherit;
font-weight: 500;
line-height: 1.35;
letter-spacing: -0.02em;
}
.kodza-copy--prompt .kodza-copy__button:not(#_) {
margin-left: auto;
padding: 0 20px 0 18px;
font-size: 15px;
}
@media (prefers-reduced-motion: reduce) {
.kodza-copy__button:not(#_) {
transition-duration: 0.01ms !important;
transition-delay: 0s !important;
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-copy__glyph:not(#_) {
transition-duration: 0.01ms !important;
transition-delay: 0s !important;
}
}
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Kodza — Copy to Clipboard
// Copies the command (or the text in data-kodza-copy="…"), confirms it on screen and
// to screen readers, then resets. Falls back to a hidden textarea where the
// Clipboard API is not available (older browsers, pages served over http).
(() => {
const SELECTOR = '[data-kodza-copy]';
const RESET_AFTER = 1800;
async function writeText(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
const area = document.createElement('textarea');
area.value = text;
area.setAttribute('readonly', '');
area.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none';
document.body.append(area);
area.select();
const ok = document.execCommand('copy');
area.remove();
return ok;
}
}
// One instance. Everything it starts is tied to `signal`, so kodza:destroy stops it all
function mount(root, signal) {
const code = root.querySelector('.kodza-copy__code');
const button = root.querySelector('.kodza-copy__button');
const status = root.querySelector('.kodza-copy__status');
if (!button) return;
let timer;
button.addEventListener('click', async () => {
const text = root.dataset.kodzaCopy || code?.textContent.trim() || '';
const ok = await writeText(text);
if (signal.aborted) return;
clearTimeout(timer);
root.classList.toggle('is-copied', ok);
if (status) status.textContent = ok ? 'Copied to clipboard' : 'Copy failed, select the text instead';
timer = setTimeout(() => {
root.classList.remove('is-copied');
if (status) status.textContent = '';
}, RESET_AFTER);
}, { signal });
signal.addEventListener('abort', () => clearTimeout(timer));
}
// Kodza lifecycle, the same in every component: `kodza:init` starts the instances inside
// event.detail.root (the whole page by default), `kodza:destroy` stops them and puts their
// markup back. For themes and apps that swap content without a page load.
const instances = new WeakMap();
const find = (scope = document) => [scope, ...scope.querySelectorAll(SELECTOR)].filter((el) => el.matches?.(SELECTOR));
const init = (scope) => find(scope).forEach((root) => {
if (root.classList.contains('is-ready')) return; // already running, maybe from another copy of this script
const controller = new AbortController();
instances.set(root, { controller, html: root.innerHTML, attrs: [...root.attributes].map((a) => [a.name, a.value]) });
mount(root, controller.signal);
root.classList.add('is-ready');
});
const destroy = (scope) => find(scope).forEach((root) => {
const instance = instances.get(root);
if (!instance) return;
instances.delete(root);
instance.controller.abort();
[...root.attributes].forEach((a) => root.removeAttribute(a.name));
instance.attrs.forEach(([name, value]) => root.setAttribute(name, value));
root.innerHTML = instance.html;
});
document.addEventListener('kodza:init', (event) => init(event.detail?.root));
document.addEventListener('kodza:destroy', (event) => destroy(event.detail?.root));
init();
// Loaded in <head>, the script runs before the markup exists: once more when it is parsed
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => init(), { once: true });
})();
'use client';
// Kodza — Copy to Clipboard, for React. From https://kodza.dev/components/copy-clipboard/
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Next to this file: copy-clipboard.css and copy-clipboard.js, the component's CSS and JS from Kodza, as they are.
// Yours to edit: the markup is plain JSX below.
// The script changes this markup once React has rendered it (it adds layers, splits text, moves
// nodes), so React must not patch it afterwards. When a prop changes, the key gives React a fresh
// element: kodza:destroy stops the script on the old one, kodza:init starts it on the new one.
import { useRef, useEffect, type ComponentPropsWithoutRef, type ComponentRef, type CSSProperties } from 'react';
import './copy-clipboard.css';
type Variant = "prompt";
export type CopyToClipboardProps = Omit<ComponentPropsWithoutRef<'div'>, "children" | "variant" | "vars"> & {
/** Modifier class(es): .kodza-copy--prompt */
variant?: Variant | Variant[];
/** Settings, as CSS variables on the root */
vars?: {
/** default: #ffffff */
"--kz-copy-bg"?: string | number;
/** default: #0a0a0a */
"--kz-copy-fg"?: string | number;
/** default: rgba(10, 10, 10, 0.45) */
"--kz-copy-muted"?: string | number;
/** default: rgba(10, 10, 10, 0.1) */
"--kz-copy-border"?: string | number;
/** default: #0a0a0a */
"--kz-copy-button-bg"?: string | number;
/** default: #ffffff */
"--kz-copy-button-fg"?: string | number;
/** default: 14px */
"--kz-copy-radius"?: string | number;
/** default: ui-monospace, "SF Mono", "Geist Mono", Menlo, monospace */
"--kz-copy-font"?: string | number;
/** default: 14px */
"--kz-copy-size"?: string | number;
/** default: "$ " */
"--kz-copy-prompt"?: string | number;
/** default: 38px */
"--kz-copy-button-height"?: string | number;
/** default: 520px */
"--kz-copy-max-width"?: string | number;
/** default: 0.45s */
"--kz-copy-duration"?: string | number;
/** default: cubic-bezier(0.22, 1, 0.36, 1) */
"--kz-copy-ease"?: string | number;
};
className?: string;
};
export function CopyToClipboard({ variant, vars, className, style, ...rest }: CopyToClipboardProps) {
const ref = useRef<ComponentRef<'div'>>(null);
const classes = ["kodza-copy", ...([] as string[]).concat(variant ?? []).map((v) => `kodza-copy--${v}`), className].filter(Boolean).join(' ');
// Everything the markup is made of: when it changes, React gets a fresh element (see the top)
const key = JSON.stringify([classes, vars, style, rest]);
// Starts the script on this element once React has rendered it, stops it when it goes
useEffect(() => {
const root = ref.current;
let live = true;
// @ts-ignore: a plain script, no types to find
import('./copy-clipboard.js').then(() => {
if (live) document.dispatchEvent(new CustomEvent('kodza:init', { detail: { root } }));
});
return () => {
live = false;
document.dispatchEvent(new CustomEvent('kodza:destroy', { detail: { root } }));
};
}, [key]);
return (
<div data-kodza-copy="" key={key} {...rest} className={classes} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
<code className="kodza-copy__code">git commit -m "works on my machine"</code>
{' '}
<button className="kodza-copy__button" type="button">
<span className="kodza-copy__icon" aria-hidden="true">
<svg className="kodza-copy__glyph kodza-copy__glyph--copy" viewBox="0 0 16 16">
<rect x="5.5" y="5.5" width="8" height="8" rx="1.5" />
<path d="M10.5 3.5v-.5a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h.5" />
</svg>
{' '}
<svg className="kodza-copy__glyph kodza-copy__glyph--done" viewBox="0 0 16 16">
<path d="M3 8.5l3.2 3L13 4.5" pathLength="1" />
</svg>
</span>
{' '}
<span className="kodza-copy__label">
<span className="kodza-copy__word">Copy</span>
{' '}
<span className="kodza-copy__word kodza-copy__word--done" aria-hidden="true">Copied</span>
</span>
</button>
{' '}
<span className="kodza-copy__status" role="status" />
</div>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.