Input Copy
One-line copyable value. Click to copy, icon swaps to a bouncy check on success, and the value highlights as you hover — pair with a short label for API keys, IDs and install snippets.
Usage
input-copy.svelte
<script lang="ts" module>
import { tv, type VariantProps } from 'tailwind-variants';
export const inputCopyVariants = tv({
base: 'group/input-copy relative inline-flex max-w-full items-center gap-1.5 overflow-hidden rounded-(--radius-control) bg-input/30 px-3 py-1.5 font-mono text-[13px] text-foreground transition-[background-color,box-shadow] outline-none hover:bg-input/50 focus-within:bg-input/50 focus-within:ring-(length:--ring-width) focus-within:ring-border has-[button:disabled]:pointer-events-none has-[button:disabled]:opacity-50',
variants: {
size: {
sm: 'h-8 text-xs',
default: 'h-9'
},
tone: {
default: '',
muted: 'bg-transparent'
}
},
defaultVariants: {
size: 'default',
tone: 'default'
}
});
export type InputCopyVariant = VariantProps<typeof inputCopyVariants>;
</script>
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { Icon } from '$lib/components/ui/icon';
import { cn, type WithElementRef } from '$lib/utils.js';
import { animate } from 'motion';
import { springs, prefersReducedMotion } from '$lib/motion';
type Props = WithElementRef<Omit<HTMLAttributes<HTMLDivElement>, 'children'>> & {
value: string;
label?: string;
size?: InputCopyVariant['size'];
tone?: InputCopyVariant['tone'];
/** Clear success state after this many ms. */
successMs?: number;
onCopy?: (value: string) => void;
};
let {
ref = $bindable(null),
value,
label,
size = 'default',
tone = 'default',
successMs = 1500,
onCopy,
class: className,
...restProps
}: Props = $props();
let copied = $state(false);
let checkRef = $state<HTMLElement | null>(null);
let timer: ReturnType<typeof setTimeout> | null = null;
async function copy() {
try {
await navigator.clipboard.writeText(value);
copied = true;
onCopy?.(value);
if (timer) clearTimeout(timer);
timer = setTimeout(() => (copied = false), successMs);
} catch {
copied = false;
}
}
$effect(() => {
if (!copied || !checkRef) return;
const transition = prefersReducedMotion() ? { duration: 0 } : springs.bouncy;
animate(checkRef, { scale: [0.6, 1] }, transition);
});
$effect(() => () => {
if (timer) clearTimeout(timer);
});
</script>
<div
bind:this={ref}
data-slot="input-copy"
data-copied={copied ? '' : undefined}
class={cn(inputCopyVariants({ size, tone }), className)}
{...restProps}
>
{#if label}
<span class="truncate pr-1 font-sans text-xs text-muted-foreground select-none">
{label}
</span>
<span aria-hidden="true" class="h-4 w-px bg-border"></span>
{/if}
<span
data-slot="input-copy-value"
class="min-w-0 flex-1 truncate tabular-nums select-all group-hover/input-copy:[&>mark]:bg-ring/20 group-hover/input-copy:[&>mark]:text-foreground"
>
<mark
class="bg-transparent text-foreground transition-colors duration-(--motion-duration-micro)"
>
{value}
</mark>
</span>
<button
type="button"
onclick={copy}
aria-label={copied ? 'Copied' : `Copy ${label ?? 'value'}`}
class={'relative inline-flex size-7 shrink-0 items-center justify-center rounded-(--radius-pill) text-muted-foreground transition-[color,background-color] duration-(--motion-duration-fast) outline-none after:absolute after:-inset-2 after:content-[""] hover:bg-substrate-hover hover:text-foreground focus-visible:ring-(length:--ring-width) focus-visible:ring-ring/50 disabled:opacity-50 [&_i]:pointer-events-none'}
>
{#if copied}
<span bind:this={checkRef} class="inline-flex text-success">
<Icon name="check-line" size="0.875rem" class="size-3.5" aria-hidden="true" />
</span>
{:else}
<Icon name="file-copy-line" size="0.875rem" class="size-3.5" aria-hidden="true" />
{/if}
</button>
</div>
With label
input-copy-labelled.svelte
<script lang="ts">
import { InputCopy } from '$lib/components/ui/input-copy';
</script>
<InputCopy label="API key" value="sk_live_...o4X2" />Sizes
input-copy-sizes.svelte
<InputCopy size="sm" value="npm i @bloom-nx/ui" />
<InputCopy value="npm i @bloom-nx/ui" />Tones
input-copy-tone.svelte
<InputCopy tone="muted" label="ID" value="usr_01HX9K..." />API reference
One-line copyable value with an icon-button trigger. Writes to navigator.clipboard; the check icon scales in on a bouncy spring (instant under reduced-motion).
| Prop | Type | Default | Description |
|---|---|---|---|
string | — | The text written to the clipboard when the button is pressed. | |
string? | — | Optional prefix label rendered in sans-serif before the value. | |
'sm' | 'default' | 'default' | Track height and typography scale. | |
'default' | 'muted' | 'default' | 'muted' uses the muted surface with no border — blends into cards. | |
number | 1500 | Milliseconds the success state stays visible before reverting. | |
(value: string) => void | — | Fires after a successful clipboard write. |