---
title: Link
description: "Astro anchors for page paths, external URLs, phone numbers, and obfuscated email."
seo:
    title: "Link for Astro — page, external, phone, and email examples"
---

```astro
---
/**
 * Link selects the anchor behavior from `href`.
 * A site path stays on this site. An http URL opens in a new tab.
 * A tel or mailto value is stored as base64 until the visitor clicks.
 */
import { Link } from "@/components/ui/link"
---

<dl class="mx-auto grid w-full max-w-sm grid-cols-[4.5rem_minmax(0,1fr)] items-center gap-x-4 gap-y-3">
  <dt class="text-muted-foreground text-sm">Page</dt>
  <dd>
    <Link href="/components/button" variant="outline" size="default">
      Button
    </Link>
  </dd>
  <dt class="text-muted-foreground text-sm">External</dt>
  <dd>
    <Link href="https://docs.astro.build" variant="outline" size="default">
      Astro docs
    </Link>
  </dd>
  <dt class="text-muted-foreground text-sm">Phone</dt>
  <dd>
    <Link href="tel:+15555550100" variant="outline" size="default">
      +1 555 555 0100
    </Link>
  </dd>
  <dt class="text-muted-foreground text-sm">Email</dt>
  <dd>
    <Link href="mailto:hello@example.com" variant="outline" size="default">
      hello@example.com
    </Link>
  </dd>
</dl>
```

## Installation

### Command

```sh
bunx --bun shadcn@latest add @gnostic/link
```

### Manual

Complete the [Installation guide](/docs/installation) first. Then copy the component files below.

No extra packages are required.

#### src/components/ui/link/index.ts

```ts
export { default as Link } from "./link.astro"

export type { LinkProps } from "./types.ts"
```

#### src/components/ui/link/link.astro

```astro
---
import type { HTMLAttributes } from "astro/types"

import { cn } from "cn"
import { buttonVariants } from "../button/index.ts"
import type { LinkProps } from "./types.ts"

/** Anchor attributes pass through. `href` is required by `LinkProps`. */
type Props = Omit<HTMLAttributes<"a">, "href"> & LinkProps

const {
  class: className,
  "class:list": classList,
  variant,
  size = "lg",
  externalInNewTab = true,
  prefetch = true,
  href: rawHref,
  title,
  target: targetProp,
  rel: relProp,
  ...rest
} = Astro.props

const href = rawHref ?? "#"

const isExternal = href.startsWith("http")
const isHash = href.startsWith("#")
const isMail = href.startsWith("mailto:")
const isTel = href.startsWith("tel:")
const isInternal = !isExternal && !isHash && !isMail && !isTel
const isContact = isMail || isTel

const target =
  targetProp ?? (isExternal && externalInNewTab ? "_blank" : undefined)

const rel =
  relProp ??
  (isExternal
    ? "noopener nofollow"
    : isInternal && prefetch
      ? "prefetch"
      : undefined)

/** Prefix site BASE_URL for root-relative internal paths. */
let parsedUrl = href
if (!isExternal && !isHash && href.startsWith("/")) {
  const basePrefix = (import.meta.env.BASE_URL ?? "/").replace(/\/$/, "")
  parsedUrl = `${basePrefix}${href}`
}

/** Obfuscate mailto/tel hrefs so scrapers don't see them in the markup. */
let dataContact: string | undefined
if (isContact) {
  parsedUrl = "#"
  dataContact = btoa(href)
}
---

<a
  data-slot="button"
  data-size={size}
  data-variant={variant ?? "default"}
  href={parsedUrl}
  title={title || undefined}
  class:list={[
    "link",
    {
      "is-external": isExternal,
      "is-internal": isInternal,
      "is-hash": isHash,
      "is-mail": isMail,
      "is-tel": isTel,
    },
    cn(buttonVariants({ variant, size }), className),
    classList,
  ]}
  data-contact={dataContact}
  target={target}
  rel={rel}
  {...rest}
>
  <slot />
</a>
<script>
  /**
   * One delegated listener for all mail/tel links. Astro already includes this
   * script once per page; delegation avoids attaching a listener to each link
   * and still works if a contact link is added after the initial render.
   */
  document.addEventListener("click", (event) => {
    const link = (event.target as Element | null)?.closest(
      "a.link.is-mail, a.link.is-tel",
    )
    if (!(link instanceof HTMLAnchorElement)) return

    const contactHrefRaw = link.getAttribute("data-contact")
    if (!contactHrefRaw) return

    event.preventDefault()
    window.location.href = atob(contactHrefRaw)
  })
</script>
```

#### src/components/ui/link/types.ts

```ts
import type { ButtonVariantProps } from '../button/button-variants.ts'

/**
 * Props for a destination anchor.
 * Standard anchor attributes such as `class`, `target`, `title`, and `rel`
 * are also accepted on the component.
 */
export interface LinkProps extends ButtonVariantProps {
    /**
     * Destination address.
     *
     * A site path stays on this site. An `http` URL opens in a new tab.
     * A `mailto:` or `tel:` value is stored until the visitor clicks.
     */
    href: string
    /**
     * Open an external URL in a new tab.
     *
     * @default true
     */
    externalInNewTab?: boolean
    /**
     * Add `rel="prefetch"` on an internal path.
     *
     * @default true
     */
    prefetch?: boolean
}
```

Optional preset: inspect a stylesheet at /presets/{style}.css, save it to src/styles/presets/{style}.css, and import it after the global stylesheet. Set data-preset="{style}" on your layout or a component container. See [Theming](/docs/theming) for details.


## Usage

```ts
import { Link } from "@/components/ui/link"
```

```astro
<Link href="/components/button">Button</Link>
<Link href="https://docs.astro.build">Astro docs</Link>
<Link href="tel:+15555550100">+1 555 555 0100</Link>
<Link href="mailto:hello@example.com">hello@example.com</Link>
```

`Link` uses the same `variant` and `size` values as [Button](/components/button).
The size default is `lg`. Use `variant="link"` for a text link.

## Destinations

`Link` reads the start of `href` and sets the anchor behavior.

- A site path stays on this site. A path that starts with `/` receives the site `BASE_URL`. The anchor gets `rel="prefetch"` unless you set `prefetch={false}`.
- An `http` or `https` URL opens in a new tab. The anchor gets `rel="noopener nofollow"`. Set `externalInNewTab={false}` to keep the same tab.
- A value that starts with `#` stays on the current page.
- A `mailto:` or `tel:` value is stored as base64 in `data-contact`. The rendered anchor uses `href="#"`. A click reads that value and opens the mail or phone app. The address stays out of the page source.

Set `target` or `rel` when a link needs a value other than these defaults.

## Props

<AutoTypeTable path="../../packages/ui/src/components/ui/link/types.ts" name="LinkProps" />

Anchor attributes such as `class`, `target`, `title`, and `rel` pass through.
`target` and `rel` replace the defaults when you set them. Read the
[installed source](/r/link.json) for the component.

## When to use Link

Use Link for a destination. Use Button for an action that stays on the page, such as submitting a form. Put the label in the default slot. `Link` accepts the Button variants, so a destination can look like a button or a text link.

## Keyboard and behavior

Link is a native anchor. Enter activates it. An external URL opens a new tab. A phone or email click uses one page script. That script decodes `data-contact` and opens the address. The same script handles contact links added after the first render.

## Theme and layout

The styles come from Button variants and the shared theme tokens. Use the default variant for a primary destination and `variant="link"` for text in a sentence. A class on one link does not change the site palette.

## Related choices

Use [Button](/components/button) for actions, [Breadcrumb](/components/breadcrumb) for a page trail, and [Navigation Menu](/components/navigation-menu) for a site menu.
