Link
Astro anchors for page paths, external URLs, phone numbers, and obfuscated email.
- Page
- Button
- External
- Astro docs
- Phone
- +1 555 555 0100
- hello@example.com
---
/**
* 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
bunx shadcn@latest add @gnostic/linknpx shadcn@latest add @gnostic/linkpnpm dlx shadcn@latest add @gnostic/linkComplete the Installation guide first. Then copy the component files below.
No extra packages are required.
Copy the files
src/components/ui/link/index.ts
export { default as Link } from "./link.astro"
export type { LinkProps } from "./types.ts"
src/components/ui/link/link.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
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
}
Add a preset (optional)
Default Coss works with the component classes. To use another design, inspect and save its CSS to src/styles/presets/. Import it after the global stylesheet, then set data-preset on your layout or a container. These files also work with JavaScript disabled.
Usage
import { Link } from "@/components/ui/link"
<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.
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 siteBASE_URL. The anchor getsrel="prefetch"unless you setprefetch={false}. - An
httporhttpsURL opens in a new tab. The anchor getsrel="noopener nofollow". SetexternalInNewTab={false}to keep the same tab. - A value that starts with
#stays on the current page. - A
mailto:ortel:value is stored as base64 indata-contact. The rendered anchor useshref="#". 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
hrefstring
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.
stringexternalInNewTab?boolean
Open an external URL in a new tab.
booleantrueprefetch?boolean
Add `rel="prefetch"` on an internal path.
booleantruesize?"default" | "icon" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "lg" | "sm" | "xl" | "xs" | null | undefined
"default" | "icon" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "lg" | "sm" | "xl" | "xs" | null | undefinedvariant?"default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | "glass" | "surface" | "destructive-outline" | null | undefined
"default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | "glass" | "surface" | "destructive-outline" | null | undefinedAnchor attributes such as class, target, title, and rel pass through.
target and rel replace the defaults when you set them. Read the
installed source 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 for actions, Breadcrumb for a page trail, and Navigation Menu for a site menu.