diff --git a/.changeset/green-snakes-sit.md b/.changeset/green-snakes-sit.md
new file mode 100644
index 0000000000..2e50c7cbcb
--- /dev/null
+++ b/.changeset/green-snakes-sit.md
@@ -0,0 +1,5 @@
+---
+'@sumup-oss/circuit-ui': minor
+---
+
+Added a new CopyButton component to enable users to copy a predefined string of text to their clipboard.
diff --git a/packages/circuit-ui/components/CopyButton/CopyButton.mdx b/packages/circuit-ui/components/CopyButton/CopyButton.mdx
new file mode 100644
index 0000000000..a8c2d2786f
--- /dev/null
+++ b/packages/circuit-ui/components/CopyButton/CopyButton.mdx
@@ -0,0 +1,41 @@
+import { Meta, Status, Props, Story } from '../../../../.storybook/components';
+import * as Stories from './CopyButton.stories';
+
+
+
+# CopyButton
+
+
+
+CopyButton copies a value to the clipboard and can render as a read-only input, a full button, or an icon button.
+
+
+
+
+## When to Use
+
+Use CopyButton when copying is the primary action, such as copying API tokens, webhook secrets, reference IDs, or shareable links.
+
+Use the input variant when users need to inspect the copied value before copying it.
+
+Use the button or icon button variants when the value itself does not need to stay visible in the UI.
+
+## How to Use
+
+Wrap your application in `ToastProvider` to display a feedback toast after a successful copy action.
+
+
+
+Use `visibleValue` to show a masked or shortened value while preserving the full copied `value`.
+
+
+
+## Variants
+
+
+
+## Content
+
+Keep the visible label clear and specific so users understand what will be copied. Use concise copy such as `"Copy token"` or `"Copy secret"`, and customize `successLabel` when the success message should refer to a specific object.
+
+For longer strings, the input variant truncates the visible text with an ellipsis while preserving the full copied value.
diff --git a/packages/circuit-ui/components/CopyButton/CopyButton.spec.tsx b/packages/circuit-ui/components/CopyButton/CopyButton.spec.tsx
new file mode 100644
index 0000000000..fe17bb5824
--- /dev/null
+++ b/packages/circuit-ui/components/CopyButton/CopyButton.spec.tsx
@@ -0,0 +1,179 @@
+/**
+ * Copyright 2026, SumUp Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import {
+ axe,
+ render,
+ screen,
+ userEvent,
+ waitFor,
+} from '../../util/test-utils.js';
+import { ToastProvider } from '../ToastContext/index.js';
+
+import { CopyButton } from './CopyButton.js';
+
+const defaultProps = {
+ label: 'API token',
+ value: 'secret-token',
+ copyLabel: 'Copy token',
+ successLabel: 'Copied to clipboard.',
+};
+
+const buttonVariants = [
+ {
+ name: 'button',
+ props: {
+ copyVariant: 'button',
+ value: 'secret-token',
+ copyLabel: 'Copy token',
+ successLabel: 'Copied to clipboard.',
+ },
+ buttonName: 'Copy token',
+ description: 'secret-token',
+ },
+ {
+ name: 'icon button',
+ props: {
+ copyVariant: 'icon-button',
+ value: 'secret-token',
+ copyLabel: 'Copy token',
+ successLabel: 'Copied to clipboard.',
+ },
+ buttonName: 'Copy token',
+ description: 'secret-token',
+ },
+] satisfies {
+ name: string;
+ props: React.ComponentProps;
+ buttonName: string;
+ description: string;
+}[];
+
+const variants = [
+ {
+ name: 'input',
+ props: defaultProps,
+ buttonName: 'Copy token',
+ },
+ ...buttonVariants,
+] satisfies {
+ name: string;
+ props: React.ComponentProps;
+ buttonName: string;
+}[];
+
+const renderWithToastProvider = (ui: React.ReactNode) =>
+ render({ui});
+
+describe('CopyButton', () => {
+ beforeEach(() => {
+ Object.defineProperty(navigator, 'clipboard', {
+ configurable: true,
+ value: { writeText: vi.fn().mockResolvedValue(undefined) },
+ });
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('should render the input variant in a readonly input', () => {
+ render();
+
+ expect(screen.getByRole('textbox')).toHaveAttribute('readonly');
+ expect(screen.getByRole('button', { name: 'Copy token' })).toBeVisible();
+ expect(screen.getByDisplayValue(defaultProps.value)).toBeVisible();
+ });
+
+ it('should render text instead of value in the input variant when provided', () => {
+ render();
+
+ expect(screen.getByDisplayValue('••••••••••••token')).toBeVisible();
+ expect(
+ screen.queryByDisplayValue(defaultProps.value),
+ ).not.toBeInTheDocument();
+ });
+
+ describe.each(variants)('as $name', ({ props, buttonName }) => {
+ it('should show a notification toast when copying succeeds', async () => {
+ const onCopy = vi.fn();
+
+ renderWithToastProvider(
+ ,
+ );
+
+ await userEvent.click(screen.getByRole('button', { name: buttonName }));
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(props.value);
+ expect(onCopy).toHaveBeenCalledTimes(1);
+ expect(await screen.findByText('Token copied')).toBeInTheDocument();
+ });
+
+ it('should have no accessibility violations', async () => {
+ const { container } = renderWithToastProvider();
+
+ const actual = await axe(container);
+
+ expect(actual).toHaveNoViolations();
+ });
+ });
+
+ describe.each(buttonVariants)('as $name', ({
+ props,
+ buttonName,
+ description,
+ }) => {
+ it('should describe the copied value when it is not visible', () => {
+ render();
+
+ expect(
+ screen.getByRole('button', { name: buttonName }),
+ ).toHaveAccessibleDescription(description);
+ });
+ });
+
+ it('should not announce success when clipboard write fails', async () => {
+ const onCopy = vi.fn();
+
+ Object.defineProperty(navigator, 'clipboard', {
+ configurable: true,
+ value: {
+ writeText: vi.fn().mockRejectedValue(new Error('clipboard denied')),
+ },
+ });
+
+ renderWithToastProvider();
+
+ await userEvent.click(screen.getByRole('button', { name: 'Copy token' }));
+
+ await waitFor(() =>
+ expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1),
+ );
+
+ expect(onCopy).not.toHaveBeenCalled();
+ expect(screen.queryByText('Copied to clipboard.')).not.toBeInTheDocument();
+ });
+
+ it('should disable copying when the value is empty', () => {
+ render();
+
+ expect(screen.getByRole('button', { name: 'Copy token' })).toHaveAttribute(
+ 'aria-disabled',
+ 'true',
+ );
+ });
+});
diff --git a/packages/circuit-ui/components/CopyButton/CopyButton.stories.tsx b/packages/circuit-ui/components/CopyButton/CopyButton.stories.tsx
new file mode 100644
index 0000000000..36255824d8
--- /dev/null
+++ b/packages/circuit-ui/components/CopyButton/CopyButton.stories.tsx
@@ -0,0 +1,209 @@
+/**
+ * Copyright 2026, SumUp Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Stack } from '../../../../.storybook/components/index.js';
+import { ToastProvider } from '../ToastContext/index.js';
+
+import { CopyButton, type CopyButtonProps } from './CopyButton.js';
+
+export default {
+ title: 'Components/CopyButton',
+ component: CopyButton,
+ tags: ['status:experimental'],
+ decorators: [
+ (Story: () => JSX.Element) => (
+
+
+
+ ),
+ ],
+ argTypes: {
+ value: {
+ control: 'text',
+ table: {
+ category: 'Copy action',
+ },
+ },
+ copyLabel: {
+ control: 'text',
+ table: {
+ category: 'Copy action',
+ },
+ },
+ successLabel: {
+ control: 'text',
+ table: {
+ category: 'Copy action',
+ },
+ },
+ onCopy: {
+ table: {
+ category: 'Copy action',
+ },
+ },
+ copyVariant: {
+ options: ['input', 'button', 'icon-button'],
+ control: { type: 'select' },
+ table: {
+ category: 'Display',
+ },
+ },
+ visibleValue: {
+ control: 'text',
+ table: {
+ category: 'Display',
+ },
+ },
+ label: {
+ table: {
+ category: 'Input',
+ },
+ },
+ hideLabel: {
+ control: 'boolean',
+ table: {
+ category: 'Input',
+ },
+ },
+ disabled: {
+ control: 'boolean',
+ table: {
+ category: 'State',
+ },
+ },
+ invalid: {
+ control: 'boolean',
+ table: {
+ category: 'State',
+ },
+ },
+ hasWarning: {
+ control: 'boolean',
+ table: {
+ category: 'State',
+ },
+ },
+ validationHint: {
+ control: 'text',
+ table: {
+ category: 'State',
+ },
+ },
+ },
+ parameters: {
+ docs: {
+ description: {
+ component:
+ 'CopyButton copies a value to the clipboard and can render as a read-only input, a button, or an icon button.',
+ },
+ },
+ },
+};
+
+const inputArgs = {
+ label: 'API token',
+ value: 'sk_live_1234567890',
+ copyLabel: 'Copy API token',
+ successLabel: 'Token copied',
+} satisfies CopyButtonProps;
+
+export const Base = (args: CopyButtonProps) => ;
+
+Base.args = inputArgs;
+
+Base.parameters = {
+ chromatic: {
+ disableSnapshot: true,
+ },
+};
+
+export const MaskedInput = (args: CopyButtonProps) => ;
+
+MaskedInput.args = {
+ ...inputArgs,
+ visibleValue: 'sk_live_******',
+};
+
+export const LongValue = (args: CopyButtonProps) => ;
+
+LongValue.args = {
+ label: 'Webhook secret',
+ value: 'whsec_4VbX8i2LwY7nQp3Rk5Tm9Uc1Fd6Hs0Za',
+ copyLabel: 'Copy secret',
+ successLabel: 'Secret copied',
+} satisfies CopyButtonProps;
+
+LongValue.parameters = {
+ chromatic: {
+ disableSnapshot: true,
+ },
+};
+
+export const Disabled = () => (
+
+
+
+
+
+);
+
+export const ValidationStates = (args: CopyButtonProps) => (
+
+
+
+
+);
+
+ValidationStates.args = inputArgs;
+
+export const AllVariants = () => (
+
+
+
+
+
+);
diff --git a/packages/circuit-ui/components/CopyButton/CopyButton.tsx b/packages/circuit-ui/components/CopyButton/CopyButton.tsx
new file mode 100644
index 0000000000..6a4f0652a5
--- /dev/null
+++ b/packages/circuit-ui/components/CopyButton/CopyButton.tsx
@@ -0,0 +1,221 @@
+/**
+ * Copyright 2026, SumUp Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use client';
+
+import { useId } from 'react';
+import { CopyPaste } from '@sumup-oss/icons';
+
+import { Button, IconButton } from '../Button/index.js';
+import { Input, type InputProps } from '../Input/index.js';
+import { useNotificationToast } from '../NotificationToast/index.js';
+import type { ClickEvent } from '../../types/events.js';
+import type { ButtonProps, IconButtonProps } from '../Button/index.js';
+import { utilClasses } from '../../styles/utility.js';
+import { idx } from '../../util/idx.js';
+
+type CommonCopyButtonProps = {
+ /**
+ * The text value copied to the clipboard.
+ */
+ value: string;
+ /**
+ * Text label for the copy action.
+ */
+ copyLabel: string;
+ /**
+ * Callback function when the value was copied.
+ */
+ onCopy?: (event: ClickEvent) => void;
+ /**
+ * Test copy shown in as a notification after a successful copy action.
+ */
+ successLabel: string;
+};
+
+type InputCopyButtonProps = CommonCopyButtonProps &
+ Omit<
+ InputProps,
+ | 'defaultValue'
+ | 'onCopy'
+ | 'passwordManagerIgnore'
+ | 'readOnly'
+ | 'renderSuffix'
+ | 'showValid'
+ | 'type'
+ | 'value'
+ > & {
+ /**
+ * The CopyButton variant.
+ *
+ * @default input
+ */
+ copyVariant?: 'input';
+ /**
+ * Optional text rendered inside the field instead of the copied value.
+ */
+ visibleValue?: string;
+ };
+
+type ButtonCopyButtonProps = CommonCopyButtonProps &
+ Omit<
+ ButtonProps,
+ | 'children'
+ | 'destructive'
+ | 'icon'
+ | 'navigationIcon'
+ | 'onClick'
+ | 'type'
+ | 'value'
+ > & {
+ /**
+ * The CopyButton variant.
+ */
+ copyVariant: 'button';
+ };
+
+type IconCopyButtonProps = CommonCopyButtonProps &
+ Omit<
+ IconButtonProps,
+ 'children' | 'icon' | 'label' | 'onClick' | 'type' | 'value'
+ > & {
+ /**
+ * The CopyButton variant.
+ */
+ copyVariant: 'icon-button';
+ };
+
+export type CopyButtonProps =
+ | InputCopyButtonProps
+ | ButtonCopyButtonProps
+ | IconCopyButtonProps;
+
+/**
+ * The CopyButton component copies a provided value to the clipboard and
+ * can render as a read-only input, a button, or an icon button.
+ */
+export function CopyButton(props: CopyButtonProps) {
+ const { setToast } = useNotificationToast();
+ const { successLabel, copyLabel, onCopy, value } = props;
+ const isCopyDisabled = Boolean(props.disabled || value.length === 0);
+ const valueDescriptionId = useId();
+
+ const handleCopy = async (event: ClickEvent) => {
+ try {
+ // eslint-disable-next-line compat/compat
+ await navigator.clipboard.writeText(value);
+ setToast({
+ body: successLabel,
+ });
+ onCopy?.(event);
+ } catch {
+ // Ignore clipboard failures so the UI does not enter a false success state.
+ }
+ };
+
+ if (props.copyVariant === 'button') {
+ const {
+ 'aria-describedby': ariaDescribedBy,
+ copyVariant,
+ successLabel: _successLabel,
+ copyLabel: _copyLabel,
+ onCopy: _onCopy,
+ value: _value,
+ ...buttonProps
+ } = props;
+
+ return (
+ <>
+
+
+ {value}
+
+ >
+ );
+ }
+
+ if (props.copyVariant === 'icon-button') {
+ const {
+ 'aria-describedby': ariaDescribedBy,
+ copyVariant,
+ successLabel: _successLabel,
+ copyLabel: _copyLabel,
+ onCopy: _onCopy,
+ value: _value,
+ ...iconButtonProps
+ } = props;
+
+ return (
+ <>
+
+ {copyLabel}
+
+
+ {value}
+
+ >
+ );
+ }
+
+ const {
+ copyVariant,
+ successLabel: _successLabel,
+ copyLabel: _copyLabel,
+ inputClassName,
+ onCopy: _onCopy,
+ visibleValue,
+ value: _value,
+ ...inputProps
+ } = props;
+ const displayText = visibleValue ?? value;
+
+ return (
+ (
+
+ {copyLabel}
+
+ )}
+ inputClassName={inputClassName}
+ />
+ );
+}
diff --git a/packages/circuit-ui/components/CopyButton/index.ts b/packages/circuit-ui/components/CopyButton/index.ts
new file mode 100644
index 0000000000..c7a08bf053
--- /dev/null
+++ b/packages/circuit-ui/components/CopyButton/index.ts
@@ -0,0 +1,18 @@
+/**
+ * Copyright 2026, SumUp Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { CopyButton } from './CopyButton.js';
+
+export type { CopyButtonProps } from './CopyButton.js';
diff --git a/packages/circuit-ui/index.ts b/packages/circuit-ui/index.ts
index 62c24fc042..9688eef283 100644
--- a/packages/circuit-ui/index.ts
+++ b/packages/circuit-ui/index.ts
@@ -76,6 +76,8 @@ export { ButtonGroup } from './components/ButtonGroup/index.js';
export type { ButtonGroupProps } from './components/ButtonGroup/index.js';
export { CloseButton } from './components/CloseButton/index.js';
export type { CloseButtonProps } from './components/CloseButton/index.js';
+export { CopyButton } from './components/CopyButton/index.js';
+export type { CopyButtonProps } from './components/CopyButton/index.js';
export { IconButton } from './components/Button/index.js';
export type { IconButtonProps } from './components/Button/index.js';
export { Toggle } from './components/Toggle/index.js';
diff --git a/skills/circuit-ui/references/components.md b/skills/circuit-ui/references/components.md
index e76c6950dd..65f40baa2b 100644
--- a/skills/circuit-ui/references/components.md
+++ b/skills/circuit-ui/references/components.md
@@ -7,6 +7,7 @@
| `Button` | `stable` | `@sumup-oss/circuit-ui` | `./components/Button/index.js` | [Read MDX reference](components/Button.mdx) |
| `ButtonGroup` | `stable` | `@sumup-oss/circuit-ui` | `./components/ButtonGroup/index.js` | [Read MDX reference](components/ButtonGroup.mdx) |
| `CloseButton` | `stable` | `@sumup-oss/circuit-ui` | `./components/CloseButton/index.js` | Not available |
+| `CopyButton` | `experimental` | `@sumup-oss/circuit-ui` | `./components/CopyButton/index.js` | [Read MDX reference](components/CopyButton.mdx) |
| `IconButton` | `stable` | `@sumup-oss/circuit-ui` | `./components/Button/index.js` | [Read MDX reference](components/IconButton.mdx) |
| `SelectorGroup` | `stable` | `@sumup-oss/circuit-ui` | `./components/SelectorGroup/index.js` | [Read MDX reference](components/SelectorGroup.mdx) |
| `Toggle` | `stable` | `@sumup-oss/circuit-ui` | `./components/Toggle/index.js` | [Read MDX reference](components/Toggle.mdx) |
diff --git a/skills/circuit-ui/references/components/CopyButton.mdx b/skills/circuit-ui/references/components/CopyButton.mdx
new file mode 100644
index 0000000000..a8c2d2786f
--- /dev/null
+++ b/skills/circuit-ui/references/components/CopyButton.mdx
@@ -0,0 +1,41 @@
+import { Meta, Status, Props, Story } from '../../../../.storybook/components';
+import * as Stories from './CopyButton.stories';
+
+
+
+# CopyButton
+
+
+
+CopyButton copies a value to the clipboard and can render as a read-only input, a full button, or an icon button.
+
+
+
+
+## When to Use
+
+Use CopyButton when copying is the primary action, such as copying API tokens, webhook secrets, reference IDs, or shareable links.
+
+Use the input variant when users need to inspect the copied value before copying it.
+
+Use the button or icon button variants when the value itself does not need to stay visible in the UI.
+
+## How to Use
+
+Wrap your application in `ToastProvider` to display a feedback toast after a successful copy action.
+
+
+
+Use `visibleValue` to show a masked or shortened value while preserving the full copied `value`.
+
+
+
+## Variants
+
+
+
+## Content
+
+Keep the visible label clear and specific so users understand what will be copied. Use concise copy such as `"Copy token"` or `"Copy secret"`, and customize `successLabel` when the success message should refer to a specific object.
+
+For longer strings, the input variant truncates the visible text with an ellipsis while preserving the full copied value.