@use 'sprucecss/scss/spruce' as *;
.copy-field {
@include layout-stack('xs');
&__input {
font-family: config('font-family-mono', $typography);
}
}
.copy-button {
flex-shrink: 0;
min-inline-size: 6.5rem;
&.is-copied {
background-color: color('success', 'alert');
border-color: color('success', 'alert');
}
}
<div class="copy-field">
<label class="form-label" for="install-command">Install command</label>
<div class="form-group--stacked">
<input class="form-control copy-field__input" id="install-command" type="text" value="npm install sprucecss" readonly>
<button class="btn btn--primary copy-button" type="button" data-copy-target="install-command">
<svg class="btn__icon copy-button__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
<span class="copy-button__label">Copy</span>
</button>
</div>
<p class="sr-only" aria-live="polite" data-copy-status></p>
</div>
(() => {
document.querySelectorAll('[data-copy-target], [data-copy-text]').forEach((button) => {
const label = button.querySelector('.copy-button__label') ?? button;
const status = button.closest('.copy-field')?.querySelector('[data-copy-status]');
const original = label.textContent;
button.addEventListener('click', async () => {
const target = document.getElementById(button.dataset.copyTarget);
const text = button.dataset.copyText ?? target?.value ?? target?.textContent ?? '';
try {
await navigator.clipboard.writeText(text);
label.textContent = 'Copied!';
button.classList.add('is-copied');
if (status) status.textContent = 'Copied to clipboard.';
setTimeout(() => {
label.textContent = original;
button.classList.remove('is-copied');
}, 2000);
} catch (error) {
target?.select?.();
if (status) status.textContent = 'Copy failed. The text is selected; press Ctrl+C to copy it.';
}
});
});
})();