# 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"`
- **Radix Slot:** <https://www.radix-ui.com/primitives/docs/utilities/slot>
- **APG Link:** <https://www.w3.org/WAI/ARIA/apg/patterns/link/>
- **Page:** <https://ui.booleanpress.com/components/link> · @booleanpress/ui 0.2.0

## 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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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>
  )
}
```

### As a router link

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

```tsx
import * as React from "react"
import { Link } from "@booleanpress/ui/link"

// A stand-in for a router's link component: it moves within the app without loading the page.
function RouterLink({
  to,
  onNavigate,
  onClick,
  children,
  ...props
}: React.ComponentProps<"a"> & { to: string; onNavigate: (to: string) => void }) {
  return (
    <a
      {...props}
      href={to}
      onClick={(event) => {
        onClick?.(event)
        if (event.defaultPrevented) return
        event.preventDefault()
        onNavigate(to)
      }}
    >
      {children}
    </a>
  )
}

export default function LinkAsRouterLink() {
  const [path, setPath] = React.useState("/mailers")

  return (
    <div className="flex flex-col gap-2 text-sm">
      <nav aria-label="Settings" className="flex gap-4">
        {["/mailers", "/organisations", "/logs"].map((to) => (
          <Link key={to} asChild size="default" variant={path === to ? "default" : "muted"}>
            <RouterLink to={to} onNavigate={setPath} aria-current={path === to ? "page" : undefined}>
              {to.slice(1, 2).toUpperCase() + to.slice(2)}
            </RouterLink>
          </Link>
        ))}
      </nav>
      <p className="text-muted-foreground">Current route: {path}</p>
    </div>
  )
}
```

### Disabled

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

```tsx
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

| Key | Behaviour |
| --- | --- |
| Enter | Follows the link. |

## API

### Link

Renders a `a` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Render the child element instead (a router's link), with this link's look and behaviour merged onto it. |
| `disabled` | `boolean` | `false` | Turns the link off: it sets `aria-disabled`, loses its address (with `asChild`, its place in the tab order) and ignores the pointer. |
| `external` | `boolean` | `false` | Opens 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" \| null` |  | `sm` 12/18, `default` 14/21, `lg` 16/24. Left out, the link takes the size of the text around it. |
| `underline` | `"always" \| "hover" \| null` | `hover` | `hover` (default) underlines on hover only; `always` keeps the underline, for links inside running text. |
| `variant` | `"default" \| "destructive" \| "muted" \| null` | `default` | The colour: `default` (the primary colour), `muted` or `destructive`. |
| `visited` | `boolean \| null` | `false` | Colours 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`.

| Token | Used for |
| --- | --- |
| `--destructive-strong` | text, underline |
| `--foreground` | text, underline |
| `--help-active` | text |
| `--muted-foreground` | text, underline |
| `--primary` | text, underline |
| `--ring` | outline |
