Copyable value
One value such as an order number, API key or wallet address with a Copy button attached, a polite confirmation, middle truncation that never changes what is copied, and an optional mask with a Show toggle.
cmp_copyable_value_01 Preview
Demonstration — no data is sent
Fit to the available width. The frame follows the height of its content; previews taller than the maximum auto-height scroll inside it.
Give this component to your coding agent Copy a prompt that fetches this exact version and palette through the PageSugar MCP server.
Using the PageSugar MCP server, fetch component cmp_copyable_value_01 version 1.0.0 with variant "neutral", first inspect its requirements and license status and confirm this project uses Svelte 5 and Tailwind CSS 4. Retrieve every manifest file, including binary assets and any manifest-only response files, preserving relative paths. Then integrate the source and follow its usage notes. Run project checks, review the browser result and report anything unverified. Do not substitute another version or invent missing files.Not connected yet? Set up the MCP server
Code
- Palette
- Neutral
- Version
- 1.0.0
- Digest
Full digest
sha256-e5968ce49b4005932dbf7be96940e00c0651ea5ccfb53a86d07abe0391de9557
This component needs all 2 files. Download the ZIP
<!--
One value (an order number, an API key, a wallet address, a coupon code) with a copy action.
Boxed, the value sits in a tinted read-only well with the Copy button attached inside the same
hairline; inline, it runs in a row of text with a small icon button. The confirmation morphs in
place and is announced politely, a failure says so and selects the text for a manual copy, and
the value stays ordinary selectable text, so it can be copied by hand before any script runs.
-->
<script lang="ts" module>
export type CopyableValueLayout = 'boxed' | 'inline';
export type CopyableValueTruncate = 'middle' | 'end' | 'wrap';
export type CopyableValueStatus = 'idle' | 'copied' | 'failed';
export interface CopyableValueResult {
ok: boolean;
}
</script>
<script lang="ts">
import { copyText } from './copy-text';
interface Props {
/** The exact text written to the clipboard. */
value: string;
/** Names the value, and the buttons: "Copy {label}", "Show {label}". */
label: string;
/** Shown instead of value when they differ, such as a shortened form. Copy still writes value. */
display?: string;
/** Monospace type for identifiers, keys and addresses. */
mono?: boolean;
/** Hide the value behind dots with a Show toggle. Visual only: the value is still in the page. */
mask?: boolean;
/** Whether a masked value is showing. Bindable. */
revealed?: boolean;
/** 'boxed' sets the value in a field with an attached Copy button; 'inline' runs it in text. */
layout?: CopyableValueLayout;
/** How a value too long for its width is shortened. 'middle' keeps the start and the last characters. */
truncate?: CopyableValueTruncate;
/** Characters kept at the end when truncate is 'middle'. */
tailLength?: number;
/** Keep the label for assistive technology only, when the context already names the value. */
hideLabel?: boolean;
/** Visible button text, and the start of its name. */
copyLabel?: string;
/** Confirmation after a successful copy. */
copiedLabel?: string;
/** Message after a failed copy. The value is selected so the person can copy it by hand. */
failedLabel?: string;
/** Shown in the field when value is empty, such as a tracking number not issued yet. */
emptyLabel?: string;
/** Start of the reveal toggle's name: "Show {label}". */
showLabel?: string;
/** Read in place of a masked value. */
hiddenLabel?: string;
/** Milliseconds before the confirmation returns to rest. 0 keeps it. Failures stay until the next try. */
resetAfter?: number;
/** Direction of the value itself. Identifiers read left to right on a right-to-left page too. */
valueDir?: 'ltr' | 'rtl' | 'auto';
/** Called after every copy attempt. */
onCopy?: (result: CopyableValueResult) => void;
/** Extra classes on the root, for placement. */
class?: string;
}
let {
value,
label,
display,
mono = false,
mask = false,
revealed = $bindable(false),
layout = 'boxed',
truncate = 'middle',
tailLength = 6,
hideLabel = false,
copyLabel = 'Copy',
copiedLabel = 'Copied',
failedLabel = 'Copy failed. Copy the text manually.',
emptyLabel = 'Not available yet',
showLabel = 'Show',
hiddenLabel = 'Hidden',
resetAfter = 2000,
valueDir = 'ltr',
onCopy,
class: className
}: Props = $props();
const uid = $props.id();
const labelId = `${uid}-label`;
const boxed = $derived(layout === 'boxed');
const hidden = $derived(mask && !revealed);
const shown = $derived(display ?? value);
/* Middle truncation splits the text in two; the ellipsis is drawn by CSS, so it is never copied. */
const tail = $derived(Math.max(0, Math.min(tailLength, Math.floor(shown.length / 2))));
/* A revealed secret is shown whole on one line that scrolls, so every character can be checked. */
const scroll = $derived(mask && revealed);
const split = $derived(!scroll && truncate === 'middle' && tail > 0 && shown.length > tail * 2);
/* When the visible text is split or differs from value, assistive technology reads value whole. */
const proxy = $derived(split || (display !== undefined && display !== value));
let status = $state<CopyableValueStatus>('idle');
let valueEl = $state<HTMLElement>();
let timer: ReturnType<typeof setTimeout> | undefined;
/* Each attempt takes a number; only the latest one still mounted may change the status. */
let attempt = 0;
let alive = true;
$effect(() => () => {
alive = false;
attempt++;
clearTimeout(timer);
});
/* A new value drops any confirmation or pending attempt that belonged to the old one. */
$effect.pre(() => {
void value;
attempt++;
clearTimeout(timer);
status = 'idle';
});
async function copy() {
if (!value) return;
const current = ++attempt;
const stale = () => !alive || current !== attempt;
clearTimeout(timer);
/* Back to rest first, so a second copy is announced again. */
status = 'idle';
const ok = await copyText(value, stale);
if (stale()) return;
status = ok ? 'copied' : 'failed';
if (ok && resetAfter > 0) timer = setTimeout(() => (status = 'idle'), resetAfter);
if (!ok) selectValue();
/* Last, so a callback that unmounts the component finds nothing left to clean up. */
onCopy?.({ ok });
}
/*
* A revealed value that is wider than the field fades at its trailing edge until it has been
* scrolled to the end, so the hidden characters announce themselves.
*/
let fade = $state(false);
$effect(() => {
const el = valueEl;
if (!scroll || !el) {
fade = false;
return;
}
const update = () => {
const rest = el.scrollWidth - el.clientWidth - Math.abs(el.scrollLeft);
fade = rest > 1;
};
update();
const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(update);
observer?.observe(el);
el.addEventListener('scroll', update, { passive: true });
return () => {
observer?.disconnect();
el.removeEventListener('scroll', update);
};
});
/* After a failure, select the visible value so Ctrl+C or the context menu copies it. */
function selectValue() {
if (!valueEl || hidden || (display !== undefined && display !== value)) return;
window.getSelection()?.selectAllChildren(valueEl);
}
</script>
{#snippet copyIcon()}
<svg class="size-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<rect x="5.5" y="5.5" width="8" height="8" rx="1.5" stroke="currentColor" stroke-width="1.5" />
<path
d="M10.5 3.25A1.25 1.25 0 0 0 9.25 2h-5.5A1.75 1.75 0 0 0 2 3.75v5.5c0 .69.56 1.25 1.25 1.25"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>
{/snippet}
{#snippet checkIcon()}
<svg class="size-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="m3.25 8.5 3 3 6.5-7"
stroke="currentColor"
stroke-width="1.75"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
{/snippet}
{#snippet alertIcon()}
<svg class="size-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<circle cx="8" cy="8" r="6.25" stroke="currentColor" stroke-width="1.5" />
<path d="M8 4.75v3.5" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" />
<circle cx="8" cy="11" r="1" fill="currentColor" />
</svg>
{/snippet}
{#snippet eyeIcon(open: boolean)}
<svg class="size-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M1.75 8S4 3.75 8 3.75 14.25 8 14.25 8 12 12.25 8 12.25 1.75 8 1.75 8Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linejoin="round"
/>
<circle cx="8" cy="8" r="2" stroke="currentColor" stroke-width="1.5" />
{#if !open}
<path d="m2.5 13.5 11-11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
{/if}
</svg>
{/snippet}
<!-- The copy and check marks share one cell; the check shows only while copied. -->
{#snippet marks()}
<span class="grid size-4 shrink-0 place-items-center" aria-hidden="true">
{#each ['idle', 'copied'] as const as mark (mark)}
<span
class={[
'col-start-1 row-start-1 grid place-items-center',
'transition-[opacity,scale] duration-200 ease-(--_enter) motion-reduce:scale-100 motion-reduce:transition-opacity',
(mark === 'copied') === (status === 'copied')
? 'scale-100 opacity-100'
: 'scale-50 opacity-0',
mark === 'copied' && 'text-(--_accent)'
]}
>
{#if mark === 'idle'}{@render copyIcon()}{:else}{@render checkIcon()}{/if}
</span>
{/each}
</span>
{/snippet}
{#snippet text()}
<!-- A revealed value scrolls sideways, so it takes focus for keyboard scrolling (WCAG 2.1.1). -->
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<span
bind:this={valueEl}
class={[
'copyable-value__value min-w-0 text-(--_ink) tabular-nums',
mono && 'font-mono',
scroll
? 'flex [scrollbar-width:none] items-center self-stretch overflow-x-auto whitespace-pre outline-(--_accent) focus-visible:outline-2 focus-visible:-outline-offset-2 pointer-coarse:min-h-11'
: truncate === 'wrap' && !hidden
? 'wrap-anywhere whitespace-pre-wrap'
: 'flex whitespace-pre'
]}
dir={valueDir}
aria-hidden={hidden || proxy ? 'true' : undefined}
tabindex={scroll ? 0 : undefined}
data-fade={fade ? '' : undefined}
>
{#if !value}
<span class="truncate text-(--_muted) select-none">{emptyLabel}</span>
{:else if hidden}
<span class="copyable-value__dots tracking-[0.12em] select-none rtl:tracking-normal"
>{'•'.repeat(12)}</span
>
{:else if split}
<span class="truncate">{shown.slice(0, -tail)}</span><span class="shrink-0"
>{shown.slice(-tail)}</span
>
{:else}
<span class={[truncate !== 'wrap' && !scroll && 'truncate']}>{shown}</span>
{/if}
</span>
{#if hidden}
<span class="sr-only">{hiddenLabel}</span>
{:else if proxy}
<span class="sr-only select-none" dir={valueDir}>{value}</span>
{/if}
{/snippet}
{#if boxed}
<div
class={['copyable-value block min-w-0', className]}
role="group"
aria-labelledby={labelId}
data-status={status}
>
<span
id={labelId}
class={hideLabel
? 'sr-only'
: 'mb-2 block text-sm leading-5 font-medium text-pretty break-words text-(--_ink)'}
>{label}</span
>
<div
class="copyable-value__field flex min-h-10 items-stretch overflow-hidden rounded-lg border border-(--_hairline) bg-(--_surface)"
>
<div
class={[
'flex min-w-0 flex-1 items-center bg-(--_well) px-3 text-sm leading-5',
truncate === 'wrap' && 'py-2'
]}
>
{@render text()}
</div>
{#if mask}
<button
type="button"
class="copyable-value__button grid w-10 shrink-0 cursor-pointer place-items-center border-s border-(--_hairline) text-(--_muted) outline-(--_accent) transition-[color,background-color] duration-150 ease-(--_ease) hover:bg-(--_hover) hover:text-(--_ink) focus-visible:rounded-md focus-visible:outline-2 focus-visible:-outline-offset-2 active:bg-(--_press) aria-pressed:text-(--_ink) motion-reduce:transition-none pointer-coarse:min-h-11 pointer-coarse:w-11"
aria-label={`${showLabel} ${label}`}
aria-pressed={revealed ? 'true' : 'false'}
onclick={() => (revealed = !revealed)}
>
{@render eyeIcon(!revealed)}
</button>
{/if}
<button
type="button"
class="copyable-value__button inline-flex shrink-0 cursor-pointer items-center gap-2 rounded-e-md border-s border-(--_hairline) px-3 text-sm leading-5 font-medium text-(--_ink) outline-(--_accent) transition-[color,background-color] duration-150 ease-(--_ease) hover:bg-(--_hover) focus-visible:rounded-md focus-visible:outline-2 focus-visible:-outline-offset-2 active:bg-(--_press) disabled:cursor-not-allowed disabled:text-(--_muted) disabled:opacity-60 disabled:hover:bg-transparent motion-reduce:transition-none pointer-coarse:min-h-11"
aria-label={`${status === 'copied' ? copiedLabel : copyLabel} ${label}`}
disabled={!value}
onclick={copy}
>
{@render marks()}
<!-- Both labels share one cell, so the button keeps the width of the longer one. -->
<span class="grid" aria-hidden="true">
<span class={['col-start-1 row-start-1', status === 'copied' && 'invisible']}
>{copyLabel}</span
>
<span class={['col-start-1 row-start-1', status !== 'copied' && 'invisible']}
>{copiedLabel}</span
>
</span>
</button>
</div>
<!--
The failure message is always laid out in an invisible layer under the live one, so the
space it needs is held from the first render and a failure moves nothing below.
-->
<div class="mt-2 grid text-[13px] leading-5">
{#snippet failure()}
<span class="grid h-lh shrink-0 place-items-center text-(--_danger)" aria-hidden="true"
>{@render alertIcon()}</span
>
<span class="min-w-0 text-pretty break-words text-(--_danger)">{failedLabel}</span>
{/snippet}
<p class="invisible col-start-1 row-start-1 flex items-start gap-1" aria-hidden="true">
{@render failure()}
</p>
<p class="col-start-1 row-start-1 flex items-start gap-1" role="status">
{#if status === 'failed'}
{@render failure()}
{:else if status === 'copied'}
<span class="sr-only">{copiedLabel}</span>
{/if}
</p>
</div>
</div>
{:else}
<span
class={[
'copyable-value inline-flex max-w-full flex-wrap items-center gap-x-1 align-middle',
className
]}
role="group"
aria-labelledby={labelId}
data-status={status}
>
<span id={labelId} class={hideLabel ? 'sr-only' : 'me-1 text-(--_muted)'}>{label}</span>
<!-- Value and buttons stay on one row; the value shortens before a button wraps. -->
<span class="flex max-w-full min-w-0 items-center gap-x-1">
<span class="flex min-w-0 items-center">{@render text()}</span>
{#if mask}
<button
type="button"
class="copyable-value__button grid size-7 shrink-0 cursor-pointer place-items-center rounded-md text-(--_muted) outline-(--_accent) transition-[color,background-color] duration-150 ease-(--_ease) hover:bg-(--_hover) hover:text-(--_ink) focus-visible:outline-2 focus-visible:outline-offset-2 active:bg-(--_press) aria-pressed:text-(--_ink) motion-reduce:transition-none pointer-coarse:size-11"
aria-label={`${showLabel} ${label}`}
aria-pressed={revealed ? 'true' : 'false'}
onclick={() => (revealed = !revealed)}
>
{@render eyeIcon(!revealed)}
</button>
{/if}
<button
type="button"
class="copyable-value__button grid size-7 shrink-0 cursor-pointer place-items-center rounded-md text-(--_muted) outline-(--_accent) transition-[color,background-color] duration-150 ease-(--_ease) hover:bg-(--_hover) hover:text-(--_ink) focus-visible:outline-2 focus-visible:outline-offset-2 active:bg-(--_press) disabled:cursor-not-allowed disabled:hover:bg-transparent motion-reduce:transition-none pointer-coarse:size-11"
aria-label={`${copyLabel} ${label}`}
disabled={!value}
onclick={copy}
>
{@render marks()}
</button>
</span>
<!-- The check in the button confirms a copy; a failure takes its own line under the row. -->
<span class="basis-full text-[13px] leading-5 text-pretty text-(--_danger)" role="status"
>{#if status === 'failed'}{failedLabel}{:else if status === 'copied'}<span class="sr-only"
>{copiedLabel}</span
>{/if}</span
>
</span>
{/if}
<style>
/* Public tokens: set --copyable-value-* on the component or any ancestor to retone it. */
.copyable-value {
--_accent: var(--copyable-value-accent, #18181b);
--_danger: var(--copyable-value-danger, #b91c1c);
--_ink: var(--copyable-value-ink, #18181b);
--_muted: var(--copyable-value-muted, #52525b);
--_hairline: var(--copyable-value-hairline, rgb(0 0 0 / 0.12));
--_surface: var(--copyable-value-surface, #ffffff);
/* Formulas: the read-only well and the button's hover and press fills follow any retone. */
--_well: color-mix(in oklab, var(--_surface), var(--_ink) 3%);
--_hover: color-mix(in oklab, var(--_surface), var(--_ink) 6%);
--_press: color-mix(in oklab, var(--_surface), var(--_ink) 11%);
--_ease: cubic-bezier(0.2, 0, 0, 1);
--_enter: cubic-bezier(0.16, 1, 0.3, 1);
}
/* The trailing-edge fade has no utility spelling; it follows the value's own direction. */
.copyable-value__value[data-fade] {
mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent);
}
.copyable-value__value[data-fade][dir='rtl'] {
mask-image: linear-gradient(to left, #000 calc(100% - 2rem), transparent);
}
/* Forced colours drop fills, so the field keeps a system-colour edge and focus stays visible. */
@media (forced-colors: active) {
.copyable-value__field {
border-color: CanvasText;
}
.copyable-value__button:focus-visible {
outline-color: Highlight;
}
}
</style>
Usage#
On this pagePass a label and the value; the component renders it as selectable text with a Copy button that writes exactly that value, confirms in place and announces the result. Masking only hides the value on screen: it is still in the page source and the DOM, so never use it to protect a secret the viewer should not have. It does not generate, rotate or fetch keys.
- Suggested location
src/lib/components/copyable-value-01
Limitations
- Copying needs JavaScript. Before hydration, or with scripts off, the value is still ordinary text that can be selected and copied by hand.
- Browsers only allow clipboard writes from a user action on a secure (https or localhost) page. Elsewhere the
execCommandfallback is tried, and if that is blocked too the failure message shows and the value is selected. - Masking is visual only. The value is in the HTML and the DOM, and Copy writes it without revealing it.
- Middle truncation is by width: the start shortens with an ellipsis and the last
tailLengthcharacters stay. Pass display for a fixed short form instead; manual selection then copies the display text. - The boxed layout lays out the failure message invisibly from the first render, so a failure moves nothing below. The inline layout keeps its row compact: a copy shows a check in the button, and a failure adds its message on a line under the row.
- A revealed masked value is shown whole on one line that scrolls sideways (and takes keyboard focus to scroll), rather than truncated.
Example
<script lang="ts">
import CopyableValue from '$lib/components/copyable-value-01/CopyableValue.svelte';
</script>
<div class="max-w-sm">
<CopyableValue label="Order number" value="HAL-2026-08417" />
<CopyableValue
label="API key"
value="hlc_live_4fT9kQ2mX8vR1nB7cW3zP6"
mono
mask
onCopy={({ ok }) => console.log(ok ? 'copied' : 'copy failed')}
/>
</div>The source imports its own files by relative path only. Examples import from $lib/...; SvelteKit 3 projects have no $lib alias by default, so import from your #lib subpath (with the .svelte extension) or a relative path instead.
Copyable value#
One value, one Copy button, one confirmation. Use it for order and invoice numbers on receipts, IDs and keys on settings pages, wallet addresses, and codes people paste somewhere else.
Boxed or inline#
layout="boxed" (the default) sets the value in a tinted read-only well with Copy attached inside
the same hairline, and holds one line under the field for a failure message. Use it on its own or
stacked in a settings panel.
layout="inline" runs the value in a row of text with a 28 px icon button, for a details list, a
table cell or a sentence. Set hideLabel when the row already names the value; the label still
names the group and the button for screen readers.
What gets copied#
Always value, exactly. display and truncation change only what is drawn:
truncate="middle"shortens by width with a CSS ellipsis and keeps the lasttailLengthcharacters, so the ellipsis is never copied, even when someone selects the text by hand.truncate="end"is a plain end ellipsis;truncate="wrap"breaks the value across lines.displaydraws a fixed short form of your own. Copy still writesvalue, but a manual selection copies the display text, so prefertruncatewhen people might select it.
When copying fails#
The Clipboard API needs a user action on a secure page. copy-text.ts tries it, then the legacy
execCommand('copy') path. If both fail, failedLabel shows and is announced, the value is
selected so Ctrl+C or the context menu copies it, and onCopy receives { ok: false }. The
failure stays until the next attempt; a success resets after resetAfter milliseconds.
copy-text.ts has no dependencies on the component. Copy it into a code block or command snippet
that needs the same behaviour.
Masking#
mask swaps the value for twelve dots and adds a Show toggle. It is a screen-sharing courtesy, not
security: the value is in the HTML, and Copy writes it without revealing it. Bind revealed to
control or read the toggle.
Props and content inputs#
On this page| Name | Type | Required | Default | Description |
|---|---|---|---|---|
value | string | Yes | None | The exact text written to the clipboard. An empty string shows emptyLabel and disables Copy. |
label | string | Yes | None | Names the value and both buttons: 'Copy {label}' and 'Show {label}'. Visible unless hideLabel is set. |
display | string | No | None | Shown instead of value when they differ, such as a shortened form. Copy still writes value, and screen readers hear value. |
mono | boolean | No | false | Monospace type for identifiers, keys and addresses. |
mask | boolean | No | false | Shows dots and a Show toggle (aria-pressed). Revealed, the value is shown whole on one line that scrolls. Visual only: the value is still in the page. |
revealed | boolean | No | false | Whether a masked value is showing. Bindable. |
layout | 'boxed' | 'inline' | No | 'boxed' | 'boxed' sets the value in a field with Copy attached; 'inline' runs it in a row of text with a small icon button. |
truncate | 'middle' | 'end' | 'wrap' | No | 'middle' | How a value too long for its width shortens. 'middle' keeps the start and the last tailLength characters; 'wrap' breaks it across lines. |
tailLength | number | No | 6 | Characters kept at the end when truncate is 'middle'. |
hideLabel | boolean | No | false | Keep the label for assistive technology only, when the row or heading already names the value. |
copyLabel | string | No | 'Copy' | Visible button text and the start of its accessible name. |
copiedLabel | string | No | 'Copied' | Confirmation after a successful copy, shown in the button (and in its name) and announced. |
failedLabel | string | No | 'Copy failed. Copy the text manually.' | Message after a failed copy, shown under the field and announced. The value is selected when it is visible. |
emptyLabel | string | No | 'Not available yet' | Shown in the field, muted, when value is empty, such as a tracking number not issued yet. |
showLabel | string | No | 'Show' | Start of the reveal toggle's name: 'Show {label}'. |
resetAfter | number | No | 2000 | Milliseconds before Copied returns to rest; 0 keeps it. A failure stays until the next attempt. |
valueDir | 'ltr' | 'rtl' | 'auto' | No | 'ltr' | Direction of the value itself. Identifiers stay left to right on a right-to-left page. |
onCopy | (result: { ok: boolean }) => void | No | None | Called after every copy attempt with whether the text reached the clipboard. |
class | string | No | None | Extra classes on the root, for placement. |
Customization#
On this pageChange every word through props and retone the field through six --copyable-value-* CSS variables. The well, hover and press fills are mixed from surface and ink, so they follow any retone.
- Accent:
--copyable-value-accentcolours the focus ring and the Copied check. The blue palette sets it to#1d4ed8; keep it at 3:1 against the surface. - Danger:
--copyable-value-dangercolours the failure mark and message. The default isred-700(6.5:1 on white). - Neutrals:
--copyable-value-inkis the label, value and button text,--copyable-value-mutedthe inline label and icon buttons,--copyable-value-hairlinethe field's edge and the divider before each button,--copyable-value-surfacethe button fill (the value well is surface mixed 3% toward ink). - Worked retone for a
#09090bband:--copyable-value-surface:#18181b;--copyable-value-ink:#fafafa;--copyable-value-muted:#a1a1aa;--copyable-value-hairline: rgb(255 255 255 / 0.12);--copyable-value-accent:#fafafa;--copyable-value-danger:#f87171. - Cream page such as a bakery's
#fbf7f0: set--copyable-value-surface:#fffdf9and leave the hairline alone; it is alpha black, so it tints with the page. - Width: the boxed field fills its container. Put it in a
max-w-smormax-w-mdcolumn, or a grid cell; stack several with a 12 px gap (the held message line adds the rest). - Inline: use layout 'inline' inside a details list, table cell or sentence, and
hideLabelwhen the row already names the value. - Long values: keep truncate 'middle' for hashes and addresses, raise
tailLengthwhen the end is what people check, or use 'wrap' for links people should read in full. - Languages: translate
copyLabel,copiedLabel,failedLabel,showLabelandhiddenLabelwith the page. KeepvalueDir'ltr' for identifiers on right-to-left pages.
Public CSS variables
| Variable | Token |
|---|---|
--copyable-value-accent | accent |
--copyable-value-danger | danger |
--copyable-value-ink | ink |
--copyable-value-muted | muted |
--copyable-value-hairline | hairline |
--copyable-value-surface | surface |
Accessibility#
On this page- The root is a group named by the label, which stays in the accessibility tree when
hideLabelhides it visually. - The Copy button is named 'Copy
{label}' (copyLabelthen label), and 'Copied{label}' while the boxed button shows Copied, so the name always starts with the visible text. Focus stays on it after a copy. - A polite
role=statusregion is in the page from the first render; it announcescopiedLabelorfailedLabelafter each attempt, and a second copy is announced again. - Success is a check mark and the word Copied, and failure an icon and a sentence, never colour alone.
- Middle-truncated or display text is hidden from assistive technology and the whole value is read from a visually hidden copy, which is excluded from manual selection so a drag-copy is not doubled.
- A masked value is hidden from assistive technology, which hears
hiddenLabel; the Show toggle keeps one name and reports its state witharia-pressed. - Buttons are 40 px tall in the boxed field (44 px on touch screens) and 28 px inline (44 px on touch screens), with a two-pixel accent focus ring.
- Your responsibility: do not rely on mask to keep a value secret, and translate the labels with the page.
Release details#
On this page- Integration
- Local interaction
- Requires client-side JavaScript to be interactive
- Server-side rendering supported
- Dependencies
- No additional runtime packages beyond Svelte and Tailwind CSS
- License
MIT. Default license approval is pending; see the license status before adopting the source.
- Version history
- 1.0.0 (Published) Current release · 21 October 2026
Only the current release is available. Keep downloaded source and its receipt if you need to use it again later.