# External Link

A link out of the platform that keeps the look of the text around it and names the destination's host before the user leaves.

```tsx
import { ExternalLink } from "@zenoxai/ui/patterns/external-link"

function ExternalLinkDemo() {
  return (
    <p className="max-w-md text-sm text-text-secondary">
      The vendor confirmed the flaw in version 7.4. See the{" "}
      <ExternalLink href="https://advisories.acme-security.example/ZX-2026-0142">
        vendor advisory
      </ExternalLink>{" "}
      for the affected builds and the fix.
    </p>
  )
}
```

## Usage

```tsx
import { ExternalLink } from "@zenoxai/ui/patterns/external-link"

<ExternalLink href={vendor.advisoryUrl}>Vendor advisory</ExternalLink>
```

Use it for every URL that leaves the platform. It inherits the font and the color of the text
around it and keeps a thin underline, so a link in running text is told apart without its color.

- **Only http and https.** The href is parsed; anything else (a relative path, `mailto:`, a
  defanged `hxxps://`, `javascript:`, a URL with credentials such as
  `https://bank.example@evil.example`) renders the children as plain text.
- **Always a new tab**, with `noopener noreferrer`: the investigation keeps its state, and the
  destination learns nothing about where the visit came from.
- **A disclosure on click.** The popover names the destination's host in monospace, in ASCII (an
  internationalized name shows its punycode beside the written form), so a lookalike shows before
  the user leaves. Copy link copies the URL; Open link opens it. Cmd, Ctrl, Shift and middle
  clicks go straight to a new tab: the user asked for one.

The disclosure is a speed bump, not a security control. Never use External Link on an indicator:
a phishing URL or a C2 host is data to copy, shown in `font-mono` with
[Value Actions](/patterns/copy-button). For URLs inside a string, use
[Linkified Text](/patterns/linkified-text). For navigation inside the app, use the router's link.

Write link text that names the destination when it is not the URL: "Vendor advisory", not
"Click here".

## Examples

### A standalone value

In a detail field or a table cell, `underline="hover"` keeps the value plain until hovered.

```tsx
import { DescriptionItem, DescriptionList } from "@zenoxai/ui/patterns/description-list"
import { ExternalLink } from "@zenoxai/ui/patterns/external-link"

function ExternalLinkStandalone() {
  return (
    <DescriptionList className="w-full max-w-md">
      <DescriptionItem label="Registrar">Acme Registrar, Inc.</DescriptionItem>
      <DescriptionItem label="Registrar URL">
        <ExternalLink
          href="https://www.acme-registrar.example"
          underline="hover"
        >
          https://www.acme-registrar.example
        </ExternalLink>
      </DescriptionItem>
      <DescriptionItem label="Abuse contact">
        abuse@acme-registrar.example
      </DescriptionItem>
    </DescriptionList>
  )
}
```

### A destination the app builds

`confirm={false}` skips the disclosure for URLs the app builds itself, such as an NVD page or the
vendor's documentation. The scheme check, the new tab and `rel` stay. A URL that came from data
keeps the disclosure.

```tsx
import { ExternalLink } from "@zenoxai/ui/patterns/external-link"

function ExternalLinkTrusted() {
  return (
    <p className="max-w-md text-sm text-text-secondary">
      Scored 9.8 by{" "}
      <ExternalLink
        href="https://nvd.nist.gov/vuln/detail/CVE-2026-21490"
        confirm={false}
      >
        CVE-2026-21490 on NVD
      </ExternalLink>
      .
    </p>
  )
}
```

## Accessibility

- The link stays a link, with `aria-haspopup="dialog"` and `aria-expanded`. When the disclosure
  opens, focus moves to the popup itself, so a screen reader reads the title and the host first
  and a repeated Enter cannot leave the platform. Tab then reaches Copy link, Open link and
  Close.
- Escape and Close return focus to the link; a click elsewhere leaves focus where the user
  clicked.
- With `confirm={false}`, the link's description says "Opens in a new tab.".
- Clicks and Enter inside the link and the disclosure never reach a clickable row around it.
- Pass translated texts through `labels`; `description` receives the host, already set.

## API reference

`import { ... } from "@zenoxai/ui/patterns/external-link"`

### ExternalLink

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| confirm | `boolean` | `true` | Shows the disclosure before leaving. Pass `false` only for URLs the app builds itself (its docs, a vendor's site); URLs from data keep it. |
| href (required) | `string \| null \| undefined` |  | The destination. Anything but an absolute http or https URL renders as text. |
| labels | `Partial<ExternalLinkLabels>` |  | Already-translated texts. English by default. |
| onCopyError | `((error: unknown) => void)` |  | Called when the URL could not be written to the clipboard. |
| underline | `"hover" \| "always"` | `always` | `always` keeps a thin underline, for links in running text; `hover` underlines on hover only, for standalone values such as a table cell. |

