Skip to content
UI

External Link

beta

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.

@zenoxai/ui/patterns/external-link Markdown

The vendor confirmed the flaw in version 7.4. See the for the affected builds and the fix.

<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

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. For URLs inside a string, use 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.

Registrar
Acme Registrar, Inc.
Abuse contact
abuse@acme-registrar.example
<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.

Scored 9.8 by CVE-2026-21490 on NVD.

<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

Generated from the TypeScript source. className, style and children are accepted everywhere and omitted below.

ExternalLink is a link out of the platform that adds behavior and almost no look: it inherits the font and color of the text around it. It links only absolute http and https URLs, always opens a new tab with `noopener noreferrer`, and on click first shows a disclosure that names the destination's host, with Copy link and Open link.

PropTypeDefault
confirmboolean

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.

true
href*string | null | undefined

The destination. Anything but an absolute http or https URL renders as text.

-
labelsPartial<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` keeps a thin underline, for links in running text; `hover` underlines on hover only, for standalone values such as a table cell.

always