Skip to content

Link

A styled <a> primitive that standardises theme color, hover and focus states, underline behavior, and external-link affordances. Polymorphic via as so it can wrap any router’s link component.

Link is styling-only, not router-aware. Use the as prop to pass react-router’s Link, TanStack Router’s Link, or Next.js’s Link for client-side navigation. Out of the box, it renders a plain anchor.

Live Preview

Import

import { Link } from 'entangle-ui';

Usage

<Link href="/docs">Documentation</Link>
<Link href="https://example.com">Auto-detected external</Link>
<Link as={RouterLink} to="/profile">Profile</Link>

Variants

Variants

VariantColorUse case
defaultAccent primaryStandalone calls-to-action
subtleSecondary text, accent on hoverStatus bars, footers, secondary actions
inlineInherits — always underlinedLinks inside prose
<Link href="/foo" variant="default">Default</Link>
<Link href="/foo" variant="subtle">Subtle</Link>
<Link href="/foo" variant="inline">Inline</Link>

Underline

Underline

<Link href="/foo" underline="always">Always</Link>
<Link href="/foo" underline="hover">Hover (default)</Link>
<Link href="/foo" underline="never">Never</Link>

variant="inline" forces underline="always" since color is no longer doing the work of distinguishing the link.

Sizes

Sizes

SizeFont sizeUse case
smtypography.fontSize.xsToolbars, status bar
mdtypography.fontSize.smDefault
lgtypography.fontSize.mdProminent CTA links

External links are auto-detected when href starts with http:// or https://. Detection adds an external-link icon, sets target="_blank" and rel="noopener noreferrer", and announces “(opens in new tab)” to assistive tech.

Pass external={false} to force same-tab navigation on an absolute URL, or external to force external behavior on a relative one.

External

<Link href="https://example.com">Auto-detected</Link>
<Link href="/internal" external>Forced external</Link>
<Link href="https://example.com" external={false}>Same-tab override</Link>

Disabled

disabled renders as a <span> regardless of as, strips navigation handlers (href, to, onClick, key/pointer events), sets aria-disabled, and turns off pointer events. This means a disabled router link cannot navigate via mouse, keyboard, or programmatic activation — aria-disabled plus CSS alone wouldn’t be enough to block router-driven triggers, so the wrapper enforces it at render time.

When disabled is true, the external-link icon and “(opens in new tab)” announcement are also suppressed, since there is no real external navigation to advertise.

Disabled

<Link href="/foo" disabled>
Disabled
</Link>

Router integration (as)

Pass any router library’s link component via as to get client-side navigation while keeping all of Link’s styling.

Polymorphic `as`

// react-router v6+
import { Link as RouterLink } from 'react-router-dom';
<Link as={RouterLink} to="/profile">
Profile
</Link>;
// TanStack Router
import { Link as RouterLink } from '@tanstack/react-router';
<Link as={RouterLink} to="/profile">
Profile
</Link>;
// Next.js (App Router)
import NextLink from 'next/link';
<Link as={NextLink} href="/profile">
Profile
</Link>;

as is the only place in the library where polymorphic rendering is supported. The pattern is too useful for Link to skip — but it’s deliberately not adopted across other components.

In prose

Use variant="inline" for links inside running text. They inherit color from the surrounding paragraph and stay underlined so they remain identifiable.

Inline in prose

<Text>
Read the{' '}
<Link href="/guide" variant="inline">
getting-started guide
</Link>{' '}
first.
</Text>

Editor example

A status bar showing two subtle sm links — typical use inside an editor footer.

Status bar

When to use

  • Link — any visible navigation in your UI, internal or external
  • <Button variant="ghost"> — actions that don’t navigate (form submit, modal trigger). Don’t reach for a styled link to fire a JavaScript handler that has no URL behind it

Accessibility

  • External links automatically get a screen-reader-only “(opens in new tab)” announcement, either appended to your existing aria-label or rendered as a hidden span next to the icon
  • The external-link icon itself is aria-hidden so it isn’t announced twice
  • disabled renders as a <span> with aria-disabled="true" so screen readers announce the state and browsers don’t follow the link
  • Focus-visible applies the theme focus ring (shadows.focus) so keyboard users can see where they are

Props

Prop Type Default Description
children * ReactNode Link content.
href string Destination URL. Omitted when `disabled` is true.
as ElementType 'a' (always 'span' when disabled) Polymorphic root override — pass a router's link component to get client-side navigation. When the resolved element is generic (e.g. a router link), its props (`to`, `replace`, …) are type-checked. Ignored when `disabled` is true.
variant 'default' | 'subtle' | 'inline' 'default' Visual style.
color 'primary' | 'secondary' | 'inherit' | string Color override. Defaults follow the variant. Any CSS color string is accepted.
underline 'always' | 'hover' | 'never' 'hover' Underline behavior. Forced to `always` for `variant="inline"`.
size 'sm' | 'md' | 'lg' 'md' Font size.
external boolean Render as external link. Auto-detected from `href` when it starts with `http://` or `https://`. Explicit prop overrides detection.
disabled boolean false Disable the link. Renders as a `<span>` with no `href`, `aria-disabled`, and `pointer-events: none`.
className string Additional CSS class names.
style CSSProperties Inline styles applied to the rendered element.
testId string Test identifier for automated testing.
ref Ref<HTMLAnchorElement> Ref to the rendered anchor.