Shadcn Clipboard for React and Tailwind
Copy text to the clipboard.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/clipboard.jsonpnpm dlx shadcn@latest add https://kit.dev/r/clipboard.jsonnpx shadcn@latest add https://kit.dev/r/clipboard.jsonyarn shadcn@latest add https://kit.dev/r/clipboard.json<Step>This component depends on Input (for ClipboardInput styles). Install it first if you haven't already.</Step>
Install the following dependencies:
bun add @ark-ui/react tailwind-variants lucide-reactpnpm add @ark-ui/react tailwind-variants lucide-reactnpm install @ark-ui/react tailwind-variants lucide-reactyarn add @ark-ui/react tailwind-variants lucide-reactCopy and paste the following code into your project.
"use client";
import {
Clipboard as ArkClipboard,
useClipboard as useArkClipboard,
useClipboardContext as useArkClipboardContext,
} from "@ark-ui/react/clipboard";
import { CheckIcon, ClipboardIcon } from "lucide-react";
import type React from "react";
import { tv, type VariantProps } from "tailwind-variants";
import { cn } from "@/lib/utils";
import { inputVariants } from "@/components/ui/input";
export const useClipboard = useArkClipboard;
export const useClipboardContext = useArkClipboardContext;
export const ClipboardContext = ArkClipboard.Context;
const clipboardControlClassName = "flex flex-wrap items-center gap-2";
interface ClipboardProps
extends React.ComponentProps<typeof ArkClipboard.Root> {
/**
* Class names on the root. `className` is applied to the baked control.
*/
rootClassName?: string;
}
export const Clipboard = (props: ClipboardProps) => {
const { rootClassName, className, children, ...rest } = props;
return (
<ArkClipboard.Root
className={cn(rootClassName)}
data-slot="clipboard"
{...rest}
>
<ArkClipboard.Control
className={cn(clipboardControlClassName, className)}
data-slot="clipboard-control"
>
{children}
</ArkClipboard.Control>
</ArkClipboard.Root>
);
};
export const ClipboardRootProvider = (
props: React.ComponentProps<typeof ArkClipboard.RootProvider> & {
/**
* Class names on the root. `className` is applied to the baked control.
*/
rootClassName?: string;
}
) => {
const { rootClassName, className, children, ...rest } = props;
return (
<ArkClipboard.RootProvider
className={cn(rootClassName)}
data-slot="clipboard"
{...rest}
>
<ArkClipboard.Control
className={cn(clipboardControlClassName, className)}
data-slot="clipboard-control"
>
{children}
</ArkClipboard.Control>
</ArkClipboard.RootProvider>
);
};
export const ClipboardLabel = (
props: React.ComponentProps<typeof ArkClipboard.Label>
) => {
const { className, ...rest } = props;
return (
<ArkClipboard.Label
className={cn("w-full font-medium text-sm", className)}
data-slot="clipboard-label"
{...rest}
/>
);
};
export const ClipboardTrigger = (
props: React.ComponentProps<typeof ArkClipboard.Trigger>
) => <ArkClipboard.Trigger data-slot="clipboard-trigger" {...props} />;
export const ClipboardInput = (
props: React.ComponentProps<typeof ArkClipboard.Input>
) => {
const { className, ...rest } = props;
return (
<ArkClipboard.Input
className={cn(inputVariants(), className)}
data-slot="clipboard-input"
{...rest}
/>
);
};
export const clipboardValueVariants = tv({
base: [
"inline-flex items-center",
"px-3",
"bg-transparent dark:bg-input/30",
"text-base md:text-sm",
"rounded-lg border border-input shadow-sm/5",
],
defaultVariants: {
size: "md",
},
variants: {
// biome-ignore assist/source/useSortedKeys: Input-matching size scale
size: {
xs: "h-6",
sm: "h-7",
md: "h-8",
lg: "h-9",
xl: "h-10",
},
},
});
interface ClipboardValueProps
extends React.ComponentProps<typeof ArkClipboard.ValueText>,
VariantProps<typeof clipboardValueVariants> {}
export const ClipboardValue = (props: ClipboardValueProps) => {
const { size, className, ...rest } = props;
return (
<ArkClipboard.ValueText
className={cn(clipboardValueVariants({ size }), className)}
data-slot="clipboard-value"
{...rest}
/>
);
};
export const ClipboardIndicator = (
props: React.ComponentProps<typeof ArkClipboard.Indicator>
) => {
const {
copied = <CheckIcon aria-hidden="true" />,
className,
children,
...rest
} = props;
return (
<ArkClipboard.Indicator
className={cn("pointer-events-none", className)}
copied={copied}
data-slot="clipboard-indicator"
{...rest}
>
{children ?? <ClipboardIcon aria-hidden="true" />}
</ArkClipboard.Indicator>
);
};Update the import paths to match your project setup.
Anatomy
Clipboard
├── ClipboardLabel
├── Control (baked in)
│ ├── ClipboardInput
│ ├── ClipboardValue
│ └── ClipboardTrigger
│ └── ClipboardIndicatorClipboard wraps children in Clipboard.Control. className is applied to that control. Use rootClassName for the root. useClipboard is the machine hook for ClipboardRootProvider; useClipboardContext / ClipboardContext is in-tree.
Usage
import { Button } from "@/components/ui/button";
import {
Clipboard,
ClipboardIndicator,
ClipboardInput,
ClipboardTrigger,
} from "@/components/ui/clipboard";<Clipboard value="https://shadcn.io">
<ClipboardInput />
<ClipboardTrigger asChild>
<Button size="icon-md">
<ClipboardIndicator />
</Button>
</ClipboardTrigger>
</Clipboard>Triggers that should look like Button use asChild with a single child. The trigger already sets aria-label to "Copy to clipboard" / "Copied to clipboard".
Controlled
Use value and onValueChange to control the string that will be copied. { value } is a string.
Root Provider
Use useClipboard with ClipboardRootProvider when you need copied, value, setValue, or copy() outside the tree. Pass value / timeout / onStatusChange to useClipboard(), not to the provider.
Context
ClipboardContext / useClipboardContext expose copied, value, setValue, and copy() inside the tree.
Examples
Label
ClipboardLabel associates with the input (htmlFor).
Icon only
Different icons
Pass copied and children to ClipboardIndicator.
Value text
ClipboardValue shows the string as text instead of an input. size matches Input heights (xs–xl).
Timeout
timeout is how long copied stays true. Ark default is 3000 ms.
Copy status
onStatusChange fires when copy starts and when the timeout clears. { copied: boolean }.
API Reference
shadcn.io wraps Ark UI Clipboard. Control is baked into Clipboard / ClipboardRootProvider. Default timeout is 3000 (not 2000). value is optional if you pass defaultValue.
asChild merges props onto a single child element.
Clipboard
Root. Renders a div. Children are wrapped in the control (flex row).
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render the child element instead of a div. |
className | string | - | Class names on the control, not the root. |
defaultValue | string | - | Uncontrolled string to copy. |
id | string | - | Unique id for the machine. |
ids | Partial<{ root: string; input: string; label: string }> | - | Element ids for composition. |
onStatusChange | (details: CopyStatusDetails) => void | - | Copy status changed. { copied: boolean }. |
onValueChange | (details: ValueChangeDetails) => void | - | Value changed. { value: string }. |
rootClassName | string | - | Class names on the root. |
timeout | number | 3000 | How long copied stays true, in ms. |
translations | { triggerLabel?: (copied: boolean) => string } | see below | Accessible name of the trigger. |
value | string | - | Controlled string to copy. |
Default translations.triggerLabel: "Copy to clipboard" / "Copied to clipboard".
| Attribute | Description |
|---|---|
data-slot | clipboard |
data-scope | clipboard |
data-part | root |
data-copied | Present while the copied state is active |
Control: data-slot="clipboard-control", data-part="control", data-copied as above.
ClipboardRootProvider
Takes the API from useClipboard. Same baked control as Clipboard.
| Prop | Type | Default | Description |
|---|---|---|---|
value | UseClipboardReturn | required | Return value of useClipboard(). |
asChild | boolean | false | Render the child element instead of a div. |
className | string | - | Class names on the control. |
rootClassName | string | - | Class names on the root. |
Pass value, timeout, onStatusChange, and other machine options to useClipboard(), not to the provider.
ClipboardLabel
Visible label for the input. Renders a label. Takes a full row in the control (w-full).
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the label. |
| Attribute | Description |
|---|---|
data-slot | clipboard-label |
data-scope | clipboard |
data-part | label |
data-copied | Present while copied |
ClipboardInput
Read-only input showing the value. Focus selects the contents. Native copy on the input also sets copied. Renders an input.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the input. Uses Input inputVariants. |
| Attribute | Description |
|---|---|
data-slot | clipboard-input |
data-scope | clipboard |
data-part | input |
data-copied | Present while copied |
data-readonly | Always "true" |
ClipboardValue
Text display of the value (ValueText). Renders a span (styled as a field).
| Prop | Type | Default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Height tokens matching Input (h-6 … h-10). |
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the text. |
| Attribute | Description |
|---|---|
data-slot | clipboard-value |
data-scope | clipboard |
data-part | value-text |
ClipboardTrigger
Copies value on click. Renders a button. Use asChild with Button.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the trigger. |
| Attribute | Description |
|---|---|
data-slot | clipboard-trigger |
data-scope | clipboard |
data-part | trigger |
data-copied | Present while copied |
ClipboardIndicator
Shows copied content while copied, otherwise children. Defaults to ClipboardIcon / CheckIcon (both aria-hidden). Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
copied | ReactNode | CheckIcon | Content while copied. |
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the indicator. |
| Attribute | Description |
|---|---|
data-slot | clipboard-indicator |
data-scope | clipboard |
data-part | indicator |
useClipboard
Creates the clipboard API for ClipboardRootProvider.
const clipboard = useClipboard({ value: "https://shadcn.io", timeout: 5000 });
clipboard.copy();ClipboardContext / useClipboardContext
Render-prop or hook access. Use inside Clipboard or ClipboardRootProvider.
| Property | Type | Description |
|---|---|---|
copied | boolean | Whether the copied timeout is active. |
value | string | String that will be copied. |
setValue | (value: string) => void | Set the string. |
copy | () => void | Copy value to the system clipboard. |
ClipboardContext children: (context) => ReactNode.
Accessibility
The trigger is a button with aria-label "Copy to clipboard" or "Copied to clipboard". Pair ClipboardInput with ClipboardLabel when the field is visible. Icon-only triggers should keep that accessible name (do not replace it with an unlabeled icon button). Decorative icons are aria-hidden.
Keyboard support
| Key | Description |
|---|---|
Tab | Move to the input or the trigger. |
Shift + Tab | Move to the previous control. |
Enter / Space | On the trigger, copy the value. |
Ctrl / Cmd + C | On the focused input, copy (also sets copied). |