Skip to the content

ComponentsMisc

Link

Takes the reader to another page or place: a text link in the primary colour, underlined on hover.

Import

import { Link } from "@booleanpress/ui/link"

Usage

Use a link to go somewhere, and a Button to do something. Link renders an <a>; pass asChild and your router's link to keep client-side navigation.

import { Link } from "@booleanpress/ui/link"

export function Help() {
  return (
    <Link href="https://example.com/docs" external>
      SMTP setup guide
    </Link>
  )
}

variant sets the colour: default (the primary colour), muted (slate, for footers and secondary places) or destructive. size sets 12, 14 or 16 px text; without it the link takes the size of the text around it, which is what a link inside a sentence wants. underline="hover" (default) underlines on hover; inside running text use underline="always", so the link does not rely on colour alone.

external opens the address in a new tab: it adds target="_blank", rel="noopener noreferrer" (kept beside your own rel values), an arrow after the text and a visually hidden "(opens in a new tab)". visited turns visited links purple. disabled sets aria-disabled, removes the address and ignores clicks; say near it why it is off.

Examples

Basic

A link in the primary colour, underlined on hover.

import { Link } from "@booleanpress/ui/link"

export default function LinkBasic() {
  return (
    <Link href="#delivery-log" size="default">
      View the delivery log
    </Link>
  )
}

External

external adds the arrow, opens a new tab and tells screen readers so.

import { Link } from "@booleanpress/ui/link"

export default function LinkExternal() {
  return (
    <div className="flex flex-col items-start gap-2">
      <Link href="https://example.com/docs/smtp" external size="default">
        SMTP setup guide
      </Link>
      <Link href="https://example.com/status" external variant="muted" size="sm">
        Service status
      </Link>
    </div>
  )
}

Muted

variant="muted" for a row of footer links.

import { Link } from "@booleanpress/ui/link"

export default function LinkMuted() {
  return (
    <footer className="flex flex-wrap gap-x-4 gap-y-1">
      <Link href="#privacy" variant="muted" size="sm">
        Privacy
      </Link>
      <Link href="#terms" variant="muted" size="sm">
        Terms
      </Link>
      <Link href="#support" variant="muted" size="sm">
        Support
      </Link>
    </footer>
  )
}

Destructive

variant="destructive" for a link that leads to removing something.

import { Link } from "@booleanpress/ui/link"

export default function LinkDestructive() {
  return (
    <p className="text-sm text-muted-foreground">
      This organisation has no active mailers.{" "}
      <Link href="#delete-organisation" variant="destructive">
        Delete the organisation
      </Link>
    </p>
  )
}

Sizes

sm, default and lg: 12, 14 and 16 px.

import { Link } from "@booleanpress/ui/link"

export default function LinkSizes() {
  return (
    <div className="flex flex-wrap items-baseline gap-4">
      <Link href="#api-keys" size="sm">
        API keys
      </Link>
      <Link href="#api-keys" size="default">
        API keys
      </Link>
      <Link href="#api-keys" size="lg">
        API keys
      </Link>
    </div>
  )
}

In a paragraph

Without a size the link takes the paragraph's; underline="always" marks it without colour.

import { Link } from "@booleanpress/ui/link"

export default function LinkInAParagraph() {
  return (
    <p className="max-w-md text-sm/normal">
      A mailer stops sending once the provider rejects its key. Create a new key in the{" "}
      <Link href="#api-keys" underline="always">
        API keys
      </Link>{" "}
      settings, then read the{" "}
      <Link href="https://example.com/docs/keys" underline="always" external>
        provider’s key guide
      </Link>{" "}
      for the scopes it needs.
    </p>
  )
}

asChild puts the look on a router's link, which keeps its own navigation; the current one has aria-current.

Disabled

disabled: no address, aria-disabled, 60% opacity, and a note saying why.

import { Link } from "@booleanpress/ui/link"

export default function LinkDisabled() {
  return (
    <div className="flex flex-col items-start gap-1">
      <Link href="#export" disabled size="default">
        Export the log
      </Link>
      <p className="text-xs text-muted-foreground">Export is available once the first delivery is logged.</p>
    </div>
  )
}

Accessibility

Semantics
A native <a href>, or the element passed with asChild. A disabled link keeps role="link" without an address, with aria-disabled="true".
Labels
Its text is its name: say where it goes ("SMTP setup guide", not "click here"). An external link's name ends with the provider's opensInNewTab string, "(opens in a new tab)"; the arrow is hidden from assistive technology.
Focus
It is in the tab order; the focus outline shows on keyboard focus. A disabled <a> has no address, so Tab skips it; with asChild it gets tabindex="-1" instead.
Known limits
  • Inside running text, the primary colour differs too little from the body text to mark a link by colour alone (WCAG 1.4.1): use underline="always" there.
  • With asChild, disabled cannot remove the child's own address or stop a router's click handler: it sets aria-disabled, takes the link out of the tab order and ignores the pointer.
  • Browsers show a visited colour only for addresses in the person's history, and only the colour can change; it has no example here because it depends on each reader's history.

Keyboard

Keyboard
KeyBehaviour
EnterFollows the link.

API

Link

Renders a a and passes it every other prop.

Link props
PropTypeDefaultDescription
asChildbooleanfalseRender the child element instead (a router's link), with this link's look and behaviour merged onto it.
disabledbooleanfalseTurns the link off: it sets aria-disabled, loses its address (with asChild, its place in the tab order) and ignores the pointer.
externalbooleanfalseOpens in a new tab: adds target="_blank", rel="noopener noreferrer", an arrow after the text and a visually hidden "(opens in a new tab)" from the provider's opensInNewTab string.
size"default" | "sm" | "lg" | nullsm 12/18, default 14/21, lg 16/24. Left out, the link takes the size of the text around it.
underline"always" | "hover" | nullhoverhover (default) underlines on hover only; always keeps the underline, for links inside running text.
variant"default" | "destructive" | "muted" | nulldefaultThe colour: default (the primary colour), muted or destructive.
visitedboolean | nullfalseColours a link to a visited address purple.

Also exported: linkVariants, the class names of Link's variants and sizes (cva), to give another element the same look.

Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.

Data attributes: data-slot="link" (Link), and data-variant, data-size, data-external.

Provider strings: opensInNewTab (BooleanUIProvider's strings).

Theming

The colours are --primary, --muted-foreground and --destructive-strong, with the underline at 40% until hover; visited links take --help-active.

The tokens its classes read. Change their values in your theme, and it follows.

Theme tokens
TokenUsed for
--destructive-strongtext, underline
--foregroundtext, underline
--help-activetext
--muted-foregroundtext, underline
--primarytext, underline
--ringoutline