Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

InputsReact

Clipboard

A button that copies a text, such as a command, a link or a code.

Rendered by React 19.3.1

import { Clipboard } from '@hamur/react/clipboard'

export default function ClipboardExample() {
  return (
    <Clipboard.Root className="clipboard" value="npm create vite@latest">
      <Clipboard.Value className="clipboard-command" />
      <Clipboard.Trigger
        className="button button-square"
        aria-label="Copy the command"
      >
        <Clipboard.Indicator className="clipboard-indicator">
          <svg className="clipboard-icon-copy" width="16" height="16" viewBox="0 0 24 24">
            <rect width="14" height="14" x="8" y="8" rx="2" />
            <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
          </svg>
          <svg className="clipboard-icon-copied" width="16" height="16" viewBox="0 0 24 24">
            <path d="M20 6 9 17l-5-5" />
          </svg>
          <svg className="clipboard-icon-failed" width="16" height="16" viewBox="0 0 24 24">
            <path d="M18 6 6 18M6 6l12 12" />
          </svg>
        </Clipboard.Indicator>
      </Clipboard.Trigger>
    </Clipboard.Root>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<Clipboard.Root>
  <Clipboard.Value />
  <Clipboard.Trigger>
    <Clipboard.Indicator />
  </Clipboard.Trigger>
</Clipboard.Root>

// Not in this example: Clipboard.Input, Clipboard.Label

Examples

A share link in a read-only Input named by a Label, an icon Trigger, and a line showing each status and its reason.

ClipboardLink.tsx
import * as React from 'react'
import { Clipboard } from '@hamur/react/clipboard'

export default function ClipboardLinkExample() {
  const [status, setStatus] = React.useState('idle')
  return (
    <div className="stack">
      <Clipboard.Root
        className="clipboard clipboard-field"
        value="https://example.com/r/42"
        onStatusChange={(next, details) =>
          setStatus(details.failure ? `${next} · ${details.failure}` : next)
        }
      >
        <Clipboard.Label className="clipboard-label">Share link</Clipboard.Label>
        <Clipboard.Input className="input clipboard-input" />
        <Clipboard.Trigger className="button button-square">
          <Clipboard.Indicator className="clipboard-indicator">
            <svg className="clipboard-icon-copy" width="16" height="16" viewBox="0 0 24 24">
              <rect width="14" height="14" x="8" y="8" rx="2" />
              <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
            </svg>
            <svg className="clipboard-icon-copied" width="16" height="16" viewBox="0 0 24 24">
              <path d="M20 6 9 17l-5-5" />
            </svg>
            <svg className="clipboard-icon-failed" width="16" height="16" viewBox="0 0 24 24">
              <path d="M18 6 6 18M6 6l12 12" />
            </svg>
          </Clipboard.Indicator>
        </Clipboard.Trigger>
      </Clipboard.Root>
      <p className="muted clipboard-log">status {status}</p>
    </div>
  )
}

Generated

A value function that makes a new ticket number on each press; the line shows the number and the status.

ClipboardGenerated.tsx
import * as React from 'react'
import { Clipboard } from '@hamur/react/clipboard'

export default function ClipboardGeneratedExample() {
  const next = React.useRef(1042)
  const [ticket, setTicket] = React.useState('none issued')
  const [status, setStatus] = React.useState('idle')
  const issue = () => {
    const text = `TICKET-${next.current}`
    next.current += 1
    setTicket(text)
    return text
  }
  return (
    <div className="stack">
      <Clipboard.Root
        className="clipboard"
        value={issue}
        onStatusChange={(value) => setStatus(value)}
      >
        <Clipboard.Trigger className="button">
          <Clipboard.Indicator className="clipboard-indicator clipboard-indicator-start">
            <svg className="clipboard-icon-copy" width="16" height="16" viewBox="0 0 24 24">
              <rect width="14" height="14" x="8" y="8" rx="2" />
              <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
            </svg>
            <svg className="clipboard-icon-copied" width="16" height="16" viewBox="0 0 24 24">
              <path d="M20 6 9 17l-5-5" />
            </svg>
            <svg className="clipboard-icon-failed" width="16" height="16" viewBox="0 0 24 24">
              <path d="M18 6 6 18M6 6l12 12" />
            </svg>
          </Clipboard.Indicator>
          Copy a new ticket number
        </Clipboard.Trigger>
      </Clipboard.Root>
      <p className="muted clipboard-log">
        {ticket} · {status}
      </p>
    </div>
  )
}

API reference

7 props across 6 parts, in React syntax. className, style, render are listed once, under Shared props.

IndicatorClipboard.Indicator

The Trigger's icon or text for each status, hidden from assistive technology: the status region says the result. Pick its content from the state with render or className, or with CSS on the data attributes. Renders a <span>.

Indicator data attributes
Data attributeDescription
data-copyingPresent while the value is being written.
data-copiedPresent after the copy succeeded, until the timeout.
data-failedPresent after the copy failed, until the timeout.
data-disabledPresent when the component is disabled.

InputClipboard.Input

A read-only text field holding the value, selected on focus so it can be copied by hand. A copy from it counts as copied. Stays focusable while the component is disabled. Renders an <input>.

Input data attributes
Data attributeDescription
data-copyingPresent while the value is being written.
data-copiedPresent after the copy succeeded, until the timeout.
data-failedPresent after the copy failed, until the timeout.
data-disabledPresent when the component is disabled.

LabelClipboard.Label

Names the Input and, with its own text, the Trigger. Renders a <label>.

Label data attributes
Data attributeDescription
data-copyingPresent while the value is being written.
data-copiedPresent after the copy succeeded, until the timeout.
data-failedPresent after the copy failed, until the timeout.
data-disabledPresent when the component is disabled.

RootClipboard.Root

Groups all parts of the clipboard: a button that copies a text, with an optional label, field and display of the text., with the status region as its last child. Renders a <div>.

Root props
PropTypeDefault
valuerequiredThe text written on a press, or a function called once per press that returns it or aClipboardValue—
timeoutThe milliseconds copied and failed last before the status returns to idle;number2000
disabledWhether the component should ignore user interaction.booleanfalse
onStatusChangeEvent handler called on every status change, copying and idle included.((status: ClipboardStatus, eventDetails: ClipboardStatusChangeEventDetails) => void)—
labelsThe texts, in English by default: the Trigger's name and the announcements.Partial<ClipboardLabels>—
Root data attributes
Data attributeDescription
data-copyingPresent while the value is being written.
data-copiedPresent after the copy succeeded, until the timeout.
data-failedPresent after the copy failed, until the timeout.
data-disabledPresent when the component is disabled.

TriggerClipboard.Trigger

A button that copies the value. Its name stays the same in every status: its text, or "Copy" when it has none, followed by the Label. Renders a <button>.

Trigger props
PropTypeDefault
focusableWhenDisabledWhether the button stays focusable when disabled.booleanfalse
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
Trigger data attributes
Data attributeDescription
data-copyingPresent while the value is being written.
data-copiedPresent after the copy succeeded, until the timeout.
data-failedPresent after the copy failed, until the timeout.
data-disabledPresent when the component is disabled.

ValueClipboard.Value

Shows the value as text; its children replace it. Renders a <span>.

Value data attributes
Data attributeDescription
data-copyingPresent while the value is being written.
data-copiedPresent after the copy succeeded, until the timeout.
data-failedPresent after the copy failed, until the timeout.
data-disabledPresent when the component is disabled.

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Clipboard has no keys of its own. The Trigger is a native button, so Enter and Space press it; Tab reaches the Input, which selects its whole value, and a copy from the keyboard there counts as copied too.

Accessibility

What the parts set
PartSetsSo that
Triggera button of type button named by its text, or "Copy" (labels) when it has none, followed by the Labelthe name says what is copied and never changes, so focus stays put and nothing is re-read
Indicatoraria-hidden="true"the icon or the word for each status is not read inside the button's name
Roota div with no role, and a visually hidden polite, atomic status region as its last child, empty until a result"Copied" or "Copy failed" is announced once, without moving focus (WCAG 4.1.3)
Inputtype="text", readonly, the value, named by the Label; it selects its value on focus and stays focusable while disabledthe text can be read, selected and copied by hand when the browser refuses the write
Labela label for the Input, and part of the Trigger's namethe field and the button are named by the same visible words
Valuea span with the valuea command or a code reads as text in the flow, with the button after it

Focus order

  1. The Input, when there is one, then the Trigger: each one Tab stop.
  2. Focus stays on the Trigger through the press, the result and the return to idle.
  3. Nothing takes focus on mount or after a result.