Skip to content
  1. Page structure
  2. Text

Text

Muted body copy for descriptions and hints, with inline links, emphasis and code.

Payouts reach your bank account two business days after a charge succeeds. You can switch to a weekly schedule at any time.

import { Text, TextLink } from '@/components/text'

export default function Example() {
  return (
    <Text>
      Payouts reach your bank account two business days after a charge succeeds. You can{' '}
      <TextLink href="/settings/payouts">switch to a weekly schedule</TextLink> at any time.
    </Text>
  )
}
<script setup lang="ts">
import Text from '@/components/text.vue'
import TextLink from '@/components/text-link.vue'
</script>

<template>
  <Text>
    Payouts reach your bank account two business days after a charge succeeds. You can
    <TextLink href="/settings/payouts">switch to a weekly schedule</TextLink> at any time.
  </Text>
</template>

Installation

Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.

import { ArrowUpRightIcon } from '@heroicons/react/16/solid'
import clsx from 'clsx'
import { Link } from './link'

const primaryText = 'text-zinc-950 dark:text-white'

const linkStyles = {
  underline: [
    primaryText,
    'underline decoration-zinc-950/50 dark:decoration-white/50 data-hover:decoration-zinc-950 dark:data-hover:decoration-white',
  ],
  action:
    'inline-flex items-center gap-1.5 text-base/6 font-medium text-accent-600 hover:text-accent-700 sm:text-sm/6 dark:text-accent-400 dark:hover:text-accent-300',
}

const newTab = { target: '_blank', rel: 'noreferrer' }

export function Text({ className, ...props }: React.ComponentPropsWithoutRef<'p'>) {
  return (
    <p
      data-slot="text"
      {...props}
      className={clsx(className, 'text-zinc-500 dark:text-zinc-400 text-base/6 sm:text-sm/6')}
    />
  )
}

export function TextLink({
  action = false,
  external = false,
  className,
  children,
  ...props
}: { action?: boolean; external?: boolean } & React.ComponentPropsWithoutRef<typeof Link>) {
  return (
    <Link {...(external && newTab)} {...props} className={clsx(className, linkStyles[action ? 'action' : 'underline'])}>
      {children}
      {external && (
        <>
          <ArrowUpRightIcon className="size-4 shrink-0 fill-current" />
          <span className="sr-only">(opens in a new tab)</span>
        </>
      )}
    </Link>
  )
}

export function Strong({ className, ...props }: React.ComponentPropsWithoutRef<'strong'>) {
  return <strong {...props} className={clsx(className, 'font-medium', primaryText)} />
}

export function Code({ className, ...props }: React.ComponentPropsWithoutRef<'code'>) {
  return (
    <code
      {...props}
      className={clsx(
        className,
        'px-0.5 rounded-sm border border-zinc-950/10 dark:border-white/20 bg-zinc-950/2.5 dark:bg-white/5',
        'font-medium text-sm sm:text-[0.8125rem]',
        primaryText
      )}
    />
  )
}
<template>
  <p data-slot="text" class="text-zinc-500 dark:text-zinc-400 text-base/6 sm:text-sm/6">
    <slot />
  </p>
</template>

Component API

PropDefaultDescription
Textrenders a <p> element
This component does not expose any component-specific props.
TextLinkrenders a <a> element
href-Destination, rendered through the kit Link component.
actionfalseStyles the link as a secondary action: accent color, medium weight, no underline.
externalfalseOpens a new tab, adds an arrow after the label and tells screen readers the link opens a new tab.
Strongrenders a <strong> element
This component does not expose any component-specific props.
Coderenders a <code> element
This component does not expose any component-specific props.
PropDefaultDescription
Textrenders a <p> element
This component does not expose any component-specific props.
TextLinkrenders a <a> element
href-Destination, rendered through the kit Link component. Only `http:`, `https:`, `mailto:`, `tel:`, `/` and `#` URLs make a link; any other value renders the text alone.
actionfalseStyles the link as a secondary action: accent color, medium weight, no underline.
externalfalseOpens a new tab, adds an arrow after the label and tells screen readers the link opens a new tab.
Strongrenders a <strong> element
This component does not expose any component-specific props.
Coderenders a <code> element
This component does not expose any component-specific props.

Usage

Text is set in a muted gray so it reads as secondary to headings and form labels. Strong, Code and TextLink switch back to full contrast, which makes the few words that matter stand out from the sentence around them. Use them sparingly: one highlight per paragraph is usually enough.

Explain what a setting does

A single sentence under a label or a title, saying what changes and for whom:

Members with the Developer role can create API keys but cannot see invoices or payouts.

import { Text } from '@/components/text'

export default function Example() {
  return <Text>Members with the Developer role can create API keys but cannot see invoices or payouts.</Text>
}
<script setup lang="ts">
import Text from '@/components/text.vue'
</script>

<template>
  <Text>Members with the Developer role can create API keys but cannot see invoices or payouts.</Text>
</template>

Stress the part people must not miss

Wrap the consequence in Strong when skipping it would cause an incident:

After you rotate the signing secret, the old one keeps working for 24 hours. Deploy the new value to every endpoint before then.

import { Strong, Text } from '@/components/text'

export default function Example() {
  return (
    <Text>
      After you rotate the signing secret, <Strong>the old one keeps working for 24 hours</Strong>. Deploy the new
      value to every endpoint before then.
    </Text>
  )
}
<script setup lang="ts">
import Strong from '@/components/strong.vue'
import Text from '@/components/text.vue'
</script>

<template>
  <Text>
    After you rotate the signing secret, <Strong>the old one keeps working for 24 hours</Strong>. Deploy the new
    value to every endpoint before then.
  </Text>
</template>

Quote a header or a route

Code frames names that people type or copy exactly, like headers, routes and identifiers:

Send an Idempotency-Key header when you retry POST /v1/payouts, so a network error never creates the same payout twice.

import { Code, Text } from '@/components/text'

export default function Example() {
  return (
    <Text>
      Send an <Code>Idempotency-Key</Code> header when you retry <Code>POST /v1/payouts</Code>, so a network error never
      creates the same payout twice.
    </Text>
  )
}
<script setup lang="ts">
import Code from '@/components/code.vue'
import Text from '@/components/text.vue'
</script>

<template>
  <Text>
    Send an <Code>Idempotency-Key</Code> header when you retry <Code>POST /v1/payouts</Code>, so a network error never
    creates the same payout twice.
  </Text>
</template>

TextLink goes through the kit Link component, so it follows your router. Inside a sentence it is underlined; the underline darkens on hover.

Link the words that say what happens on the other side, not a generic “here”, so the link still makes sense read on its own:

Payouts reach your bank account two business days after a charge succeeds. You can switch to a weekly schedule at any time.

import { Text, TextLink } from '@/components/text'

export default function Example() {
  return (
    <Text>
      Payouts reach your bank account two business days after a charge succeeds. You can{' '}
      <TextLink href="/settings/payouts">switch to a weekly schedule</TextLink> at any time.
    </Text>
  )
}
<script setup lang="ts">
import Text from '@/components/text.vue'
import TextLink from '@/components/text-link.vue'
</script>

<template>
  <Text>
    Payouts reach your bank account two business days after a charge succeeds. You can
    <TextLink href="/settings/payouts">switch to a weekly schedule</TextLink> at any time.
  </Text>
</template>

Leave the app

external opens the page in a new tab with rel=noreferrer and adds an arrow after the label, so people know before they click:

Signatures use HMAC with SHA-256, as described in the signature reference(opens in a new tab).

<Text>
  Signatures use HMAC with SHA-256, as described in the{' '}
  <TextLink external href="https://docs.example.com/signatures">
    signature reference
  </TextLink>
  .
</Text>
<Text>
  Signatures use HMAC with SHA-256, as described in the
  <TextLink external href="https://docs.example.com/signatures">signature reference</TextLink>.
</Text>

Secondary action beside a button

action drops the underline for the accent color and a medium weight, so the link reads as the second choice next to a primary button. It combines with external:

<div className="flex flex-wrap items-center gap-4">
  <Button color="accent" href="/webhooks/new">
    Add endpoint
  </Button>
  <TextLink action external href="https://docs.example.com/webhooks">
    Webhook guide
  </TextLink>
</div>
<div class="flex flex-wrap items-center gap-4">
  <Button color="accent" href="/webhooks/new">Add endpoint</Button>
  <TextLink action external href="https://docs.example.com/webhooks">Webhook guide</TextLink>
</div>