sveltelean Primitives
Versionv0.8.0 GitHub

Example

An id and a command
inv_8f2a1c
<code>inv_8f2a1c</code>
<button type="button" data-slean="button" data-variant="ghost" data-icon-only
	data-slean-copy="inv_8f2a1c" aria-label="Copy the invoice id"
	data-slean-hint="Copy" data-slean-hint-copied="Copied">
	<span data-slean-part="icon"><svg …></svg></span>
</button>

<button type="button" data-slean="button" data-variant="outline"
	data-slean-copy="yarn add @svelte-lean/primitives">
	<span data-slean-part="icon"><svg …></svg></span>
	Copy the install command
</button>
Click a button, or Tab to it and press Enter: the icon turns into a check mark while the button is marked copied, and the hint of the icon button reads Copied meanwhile. The clipboard needs a secure context.

Why this implementation exists

A copy button is a click handler, a clipboard call and a moment of feedback. Written per button, it is a function and a timer for every id in a table. The browser owns the clipboard and the button; what it has no attribute for is the value to write and the feedback after it.

The behavior registers one click handler with the shared router for every copy element on the page. The copied state is an attribute on the element, data-slean-copied, removed by the timer of the latest copy; nothing is kept per element (tests/copy.test.ts keeps one click listener for a thousand copy elements).

The browser owns

  • the clipboard: writing the text, the permission and the secure-context rule
  • the button: focus, Enter and Space, disabled

Svelte Lean owns

  • one shared click listener for every copy element on the page; no state per element
  • the copied mark: data-slean-copied for a moment after a successful copy, restarted by a second copy
  • slean:copy with the value; a copied label in a shown hint
  • leaving a click on a nested link or button to that control; the innermost copy element wins
  • copy.css: the pointer, and the check mark in place of the icon while copied; development validation

Usage

Install @svelte-lean/primitives for the behavior and @svelte-lean/styles for the stylesheet. With @svelte-lean/vite the registration is injected for every data-slean-copy attribute in the markup, with a static or a dynamic value; without it, import the register module once.

npm install @svelte-lean/primitives
+layout.svelte or any client module
import '@svelte-lean/primitives/copy/register';
stylesheets
import '@svelte-lean/styles/copy.css';

Put data-slean-copy="<value>" on a <button type="button">: the button makes the copy reachable from the keyboard. Any other element works for the pointer only, and development validation warns about it.

Keep the value visible in the page, not only in the attribute, so it can be selected by hand where the clipboard is unavailable. With data-slean-hint and data-slean-hint-copied (the Hint primitive), a shown hint reads the copied label while the button is marked.

A click on a link, button or field inside a copy element belongs to that control; when copy elements nest, the innermost one copies its own value.

invoices.svelte
<script lang="ts">
	import type { CopyDetail } from '@svelte-lean/primitives/copy';

	let list: HTMLElement;
	let status = $state('');

	$effect(() => {
		// slean:copy bubbles from the element once the value is on the clipboard. One listener on
		// an ancestor serves every copy element inside it.
		const oncopy = (event: Event) => {
			status = `Copied ${(event as CustomEvent<CopyDetail>).detail.value}`;
		};
		list.addEventListener('slean:copy', oncopy);
		return () => list.removeEventListener('slean:copy', oncopy);
	});
</script>

<ul bind:this={list}>…</ul>
<!-- The behavior adds no ARIA; a live region announces the copy to assistive technology. -->
<p role="status">{status}</p>

Anatomy

PartElementdata-slean-partRequiredNotes
rootany element with data-slean-copy="…"–yesA <button type="button"> is recommended: it makes the copy reachable from the keyboard. With data-slean-hint and data-slean-hint-copied the hint reads the copied label meanwhile.
icon<span data-slean-part="icon">iconstyles onlyA wrapper around the copy icon. copy.css swaps it for a check mark while the element is copied.

Runtime profile

The copy registers one click handler with the shared router. A copy writes one attribute on the element and starts one timer that removes it; a second copy within the duration re-stamps the attribute, so only the latest timer clears it. No layout is read.

Accessibility contract

  • The element is a native <button>: focusable, activated by Enter and Space. The behavior never moves focus.
  • The behavior adds no ARIA. The success is shown visually (data-slean-copied, the check mark, the copied hint); an application that must announce it listens for slean:copy and writes to a live region.
  • A disabled element (disabled, aria-disabled="true") copies nothing.
  • An icon-only copy button needs its own accessible name (aria-label); the hint is supplementary.

Keyboard

KeyWhenResult
Enter/Spacefocus on a <button> copy elementActivates the button (native), which copies the value

Platform features

FeatureBaselineOutside the target
Async Clipboard APIBaseline widely available since 2020 (Chrome 66, Firefox 63, Safari 13.1)Secure contexts only; elsewhere a click copies nothing and dispatches nothing
<button> keyboard activationBaseline widely availableAny other element works for the pointer only

Without JavaScript

Clicking copies nothing. The value stays in the page, where it can be selected and copied by hand.

Server rendering

Render the attributes as they are. Nothing runs on the server or at import.

Before hydration

Before the behavior loads, a click copies nothing. The registration attaches no per-element state, so the first click after hydration copies.

Styling

copy.css sets the pointer cursor (not-allowed when disabled) and, while data-slean-copied is present, hides the svg inside [data-slean-part="icon"] and shows the check mark mask from --slean-icon-check in the success color. The button keeps its own look from button.css. Select on the presence of data-slean-copied; its value is an internal stamp. The tokens it reads:

TokenDefault (light)Applies to
--slean-successoklch(55% 0.15 150)color
--slean-icon-checkurl("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5l3 3 6-7' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E")mask

State selectors the stylesheet targets, all from the platform or ARIA: :disabled, [aria-disabled="true"].

Variant attributes: data-slean-copy; data-slean-copied.

Controlled integration

copyValue(element, value?) runs the same copy from code: it writes the value (the element’s data-slean-copy by default), marks the element, redraws its hint and dispatches slean:copy, and resolves to whether the value was written.

share.ts
import { copyValue } from '@svelte-lean/primitives/copy';

// The same copy from code: marks the element, redraws its hint and dispatches slean:copy.
// Resolves to false when the clipboard is missing or refused.
const copied = await copyValue(button, link.href);

Compatibility notes

The Async Clipboard API is Baseline widely available and works in secure contexts only (https or localhost); Safari and Firefox require a user activation, which the click provides. A missing or refused clipboard sets no mark, dispatches no event and logs nothing. There is no fallback through document.execCommand('copy'), no rich clipboard content and no reading from the clipboard.

Examples

Ids in a list

Each id is the text of its own copy button, so it stays visible and selectable by hand. The status line below the list is application code: one slean:copy listener on the list writes the copied value into a role="status" region, which is how a copy reaches assistive technology.

  • Amara Okafor
  • Jonas Lindqvist
  • Mei Tanaka

invoices.svelte
{#each invoices as invoice (invoice.id)}
	<li>
		{invoice.customer}
		<!-- The id is the visible text, so it can be selected by hand without JavaScript. -->
		<button type="button" data-slean="button" data-variant="ghost" data-size="sm"
			data-slean-copy={invoice.id} data-slean-hint="Copy" data-slean-hint-copied="Copied">
			<code>{invoice.id}</code>
		</button>
	</li>
{/each}

Testing

Source