Copy Email to Clipboard Button
A button that copies an email address to the clipboard using the Clipboard API. Three stacked text spans slide up through an overflow-hidden container to show idle, hover, and copied states. Supports click, keyboard (Enter/Space), focus, and mouseleave interactions with full aria-label updates.
Code
<div class="copy-email-wrapper">
<p class="copy-email-eyebrow">Reach out to us:</p>
<button aria-label="Copy email to clipboard" data-copy-button="" data-copy-email="" class="copy-email-button">
<div class="copy-email-icon__wrap">
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 24 24" fill="none">
<path d="M16 2H8V5H16V2Z" stroke="currentColor" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M16 3H17.5L19 4.5V19.5L17.5 21H6.5L5 19.5V4.5L6.5 3H8" stroke="currentColor" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</div>
<div class="copy-email-text__wrap">
<span data-copy-email-element="" class="copy-email-text__el">hello@osmo.supply</span>
<span class="copy-email-text__el">Click to copy email</span>
<span class="copy-email-text__el">Copied to clipboard!</span>
</div>
</button>
</div>.copy-email-wrapper {
grid-column-gap: 2em;
grid-row-gap: 2em;
flex-flow: column;
justify-content: center;
align-items: center;
display: flex;
}
.copy-email-eyebrow {
opacity: .5;
text-align: center;
margin-bottom: 0;
font-size: 1em;
line-height: 1;
}
.copy-email-button {
grid-column-gap: .75em;
grid-row-gap: .75em;
background-color: #fff;
border-radius: .5em;
justify-content: flex-start;
align-items: center;
padding: .75em 1em .75em .75em;
display: flex;
}
.copy-email-button:focus {
outline-offset: 0px;
border: 1px #000;
outline: 3px #131313;
}
.copy-email-button:focus-visible,
.copy-email-button[data-wf-focus-visible] {
outline-offset: 4px;
border-style: none;
outline: 2px solid #fff;
}
.copy-email-text__wrap {
flex-flow: column;
justify-content: flex-start;
align-items: center;
height: 1.2em;
font-size: 2em;
line-height: 1.2;
display: flex;
overflow: hidden;
}
.copy-email-text__el {
white-space: nowrap;
font-size: 1em;
transition: transform .45s cubic-bezier(.65, 0, 0, 1);
}
.copy-email-icon__wrap {
color: #fff;
background-color: #22502e;
border-radius: .375em;
justify-content: center;
align-items: center;
width: 2.5em;
height: 2.5em;
padding: .5em;
transition: background-color .2s;
display: flex;
}
/* Hover styling */
@media (hover: hover) {
.copy-email-button:hover .copy-email-icon__wrap {
background: rgba(34, 80, 46, 0.9);
}
.copy-email-button:hover .copy-email-text__el {
transform: translate(0px, -100%);
}
}
/* Keyboard focus styling */
.copy-email-button:focus .copy-email-icon__wrap {
background: rgba(34, 80, 46, 0.9);
}
.copy-email-button:focus .copy-email-text__el {
transform: translate(0px, -100%);
}
/* 'Copied' state, when a user has clicked the button */
[data-copy-button="copied"] .copy-email-icon__wrap { background: #0F8E2E !important; }
[data-copy-button="copied"] .copy-email-text__el { transform: translate(0px, -200%) !important; }
@media screen and (max-width: 479px) {
.copy-email-text__wrap {
font-size: 1em;
}
}function initCopyEmailClipboard() {
const buttons = document.querySelectorAll('.copy-email-button');
if (!buttons.length) return;
const copyEmail = (button) => {
// Email to copy is taken from the data attribute on the button itself,
// or if that's empty, from a text element inside the button
const email =
button.getAttribute('data-copy-email') ||
button.querySelector('[data-copy-email-element]').textContent.trim();
if (email) {
navigator.clipboard.writeText(email).then(() => {
button.setAttribute('data-copy-button', 'copied');
button.setAttribute('aria-label', 'Email copied to clipboard!');
});
}
};
const handleInteraction = (e) => {
if (
e.type === 'click' ||
(e.type === 'keydown' && (e.key === 'Enter' || e.key === ' '))
) {
e.preventDefault();
copyEmail(e.currentTarget);
}
};
buttons.forEach((button) => {
button.addEventListener('click', handleInteraction);
button.addEventListener('keydown', handleInteraction);
button.addEventListener('mouseleave', () => {
// Remove 'active' attribute to reset color and text transform
button.removeAttribute('data-copy-button');
// Remove focus on mouseleave to clear keyboard focus styling
button.blur();
button.setAttribute('aria-label', 'Copy email to clipboard');
});
button.addEventListener('blur', () => {
button.removeAttribute('data-copy-button');
button.setAttribute('aria-label', 'Copy email to clipboard');
});
});
}
// Initialize Copy Email to Clipboard Button
document.addEventListener('DOMContentLoaded', () => {
initCopyEmailClipboard();
});Guide
Text States
Three spans stack inside an overflow:hidden container with height fixed at 1.2em. CSS transforms shift all three spans up together: rest shows the email, hover shows 'Click to copy email', and the copied state shows 'Copied to clipboard!' by translating -200%.
Email Source
The email to copy is read from data-copy-email on the button element. If that attribute is empty, the script falls back to the text content of the element with data-copy-email-element inside the button.
Event Handling
Click and keydown (Enter or Space) both trigger the copy. mouseleave and blur both reset the copied state and restore the default aria-label, so the button is ready for the next interaction.
Accessibility
The button uses aria-label to announce its state to screen readers. On copy it updates to 'Email copied to clipboard!', and resets to 'Copy email to clipboard' on mouseleave or blur.
Copied State
Adding data-copy-button="copied" to the button drives both the CSS color change on the icon and the text slide via attribute selectors, keeping all visual state in CSS rather than JS.