Skip to content

Token Field

A read-only, masked monospace input with reveal and copy-to-clipboard actions for secrets such as bot tokens.

A read-only, masked monospace input with reveal and copy-to-clipboard actions for secrets such as bot tokens. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.

Root class newt-token-field
import { Label } from "@/registry/bases/newt/ui/form-fields"
import { TokenField } from "@/registry/bases/newt/ui/token-field"

export default function TokenFieldDemo() {
  return (
    <div className="flex w-full max-w-md flex-col gap-2">
      <Label htmlFor="newt-token-example">Bot token</Label>
      <TokenField
        id="newt-token-example"
        value="MTA4NzY1NDMyMTA5ODc2NTQzMjE.GxYzAb.c0d3-s3cr3t-t0k3n"
      />
    </div>
  )
}
<script setup lang="ts">
import { Label } from "~~/registry/bases/newt/ui/form-fields"
import { TokenField } from "~~/registry/bases/newt/ui/token-field"
</script>

<template>
  <div class="flex w-full max-w-md flex-col gap-2">
    <Label for="newt-token-example">Bot token</Label>
    <TokenField
      id="newt-token-example"
      value="MTA4NzY1NDMyMTA5ODc2NTQzMjE.GxYzAb.c0d3-s3cr3t-t0k3n"
    />
  </div>
</template>

Installation

pnpm dlx newtui@latest add token-field
npx newtui@latest add token-field
yarn dlx newtui@latest add token-field
bunx newtui@latest add token-field

Add the components it builds on

Copy the source into your project

"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import { Button } from "@/registry/bases/newt/ui/button"
import { Input } from "@/registry/bases/newt/ui/form-fields"

export interface TokenFieldProps extends Omit<
  React.ComponentProps<"div">,
  "children"
> {
  /** The secret value. Masked until revealed. */
  value: string
  /** Id forwarded to the input so a label can target it. */
  id?: string
  /** Accessible name for the input when no visible label is linked. */
  name?: string
  /** Label of the reveal button while hidden. */
  revealLabel?: string
  /** Label of the reveal button while shown. */
  hideLabel?: string
  /** Label of the copy button. */
  copyLabel?: string
  /** Label of the copy button right after copying. */
  copiedLabel?: string
  /** How long (ms) the copied label is shown. */
  copiedDuration?: number
  /** Called after the value is written to the clipboard. */
  onCopy?: () => void
}

const TokenField = React.forwardRef<HTMLDivElement, TokenFieldProps>(
  (
    {
      className,
      value,
      id,
      name,
      revealLabel = "Reveal",
      hideLabel = "Hide",
      copyLabel = "Copy",
      copiedLabel = "Copied!",
      copiedDuration = 1500,
      onCopy,
      ...props
    },
    ref
  ) => {
    const [revealed, setRevealed] = React.useState(false)
    const [copied, setCopied] = React.useState(false)
    const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)

    React.useEffect(
      () => () => {
        if (timeoutRef.current) clearTimeout(timeoutRef.current)
      },
      []
    )

    const handleCopy = async () => {
      await navigator.clipboard.writeText(value)
      onCopy?.()
      setCopied(true)
      if (timeoutRef.current) clearTimeout(timeoutRef.current)
      timeoutRef.current = setTimeout(() => setCopied(false), copiedDuration)
    }

    return (
      <div ref={ref} className={cn("flex gap-2", className)} {...props}>
        <Input
          id={id}
          name={name}
          type={revealed ? "text" : "password"}
          value={value}
          readOnly
          className="font-mono tracking-[0.05em]"
        />
        <Button
          type="button"
          variant="secondary"
          aria-pressed={revealed}
          onClick={() => setRevealed((r) => !r)}
        >
          {revealed ? hideLabel : revealLabel}
        </Button>
        <Button
          type="button"
          variant="secondary"
          aria-live="polite"
          onClick={() => void handleCopy()}
        >
          {copied ? copiedLabel : copyLabel}
        </Button>
      </div>
    )
  }
)
TokenField.displayName = "TokenField"

export { TokenField }
<script setup lang="ts">
import { onBeforeUnmount, ref, type HTMLAttributes } from "vue"
import { Button } from "~~/registry/bases/newt/ui/button"
import { Input } from "~~/registry/bases/newt/ui/form-fields"

import { cn } from "@/lib/utils"

const props = withDefaults(
  defineProps<{
    /** The secret value. Masked until revealed. */
    value: string
    /** Id forwarded to the input so a label can target it. */
    id?: string
    name?: string
    revealLabel?: string
    hideLabel?: string
    copyLabel?: string
    copiedLabel?: string
    /** How long (ms) the copied label is shown. */
    copiedDuration?: number
    class?: HTMLAttributes["class"]
  }>(),
  {
    revealLabel: "Reveal",
    hideLabel: "Hide",
    copyLabel: "Copy",
    copiedLabel: "Copied!",
    copiedDuration: 1500,
  }
)

const emit = defineEmits<{
  (e: "copy"): void
}>()

const revealed = ref(false)
const copied = ref(false)
let timeout: ReturnType<typeof setTimeout> | null = null

async function copy() {
  await navigator.clipboard.writeText(props.value)
  emit("copy")
  copied.value = true
  if (timeout) clearTimeout(timeout)
  timeout = setTimeout(() => (copied.value = false), props.copiedDuration)
}

onBeforeUnmount(() => {
  if (timeout) clearTimeout(timeout)
})
</script>

<template>
  <div :class="cn('flex gap-2', props.class)">
    <Input
      :id="props.id"
      :name="props.name"
      :type="revealed ? 'text' : 'password'"
      :value="props.value"
      readonly
      class="font-mono tracking-[0.05em]"
    />
    <Button
      type="button"
      variant="secondary"
      :aria-pressed="revealed"
      @click="revealed = !revealed"
    >
      {{ revealed ? props.hideLabel : props.revealLabel }}
    </Button>
    <Button type="button" variant="secondary" aria-live="polite" @click="copy">
      {{ copied ? props.copiedLabel : props.copyLabel }}
    </Button>
  </div>
</template>
export { default as TokenField } from "./TokenField.vue"

Update the import paths

The files import @/lib/utils and sibling components through the aliases in your components.json. Point them at wherever those live in your project.

Usage

import { TokenField } from "@/components/ui/token-field"

export function Example() {
  return <TokenField />
}
<script setup lang="ts">
import { TokenField } from "@/components/ui/token-field"
</script>

<template>
  <TokenField />
</template>

API reference

TokenField

PropType
value?string

The secret value. Masked until revealed.

Typestring
id?string | undefined

Id forwarded to the input so a label can target it.

Typestring | undefined
name?string | undefined

Accessible name for the input when no visible label is linked.

Typestring | undefined
revealLabel?string | undefined

Label of the reveal button while hidden.

Typestring | undefined
hideLabel?string | undefined

Label of the reveal button while shown.

Typestring | undefined
copyLabel?string | undefined

Label of the copy button.

Typestring | undefined
copiedLabel?string | undefined

Label of the copy button right after copying.

Typestring | undefined
copiedDuration?number | undefined

How long (ms) the copied label is shown.

Typenumber | undefined
onCopy?() => void | undefined

Called after the value is written to the clipboard.

Type() => void | undefined

The registry exports TokenField. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.

Accessibility

Label the token input, announce copy feedback through a polite live region, and never make the reveal or copy actions icon-only without an accessible name.

Tokens

Token Field reads its colours, radii and motion from the --newt-* tokens installed with the library. Override a token and every component that reads it follows, including this one — see the token layer. In the plain HTML and CSS distribution the same component is newt-token-field.