DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

Link

A semantic anchor for navigation — in prose or out of it. Three variants, five sizes, five intents, three tones, a derived underline, safe external-link handling with a screen-reader warning, an opt-in visited state, icon slots, and router integration.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { Link } from "@tessinaui/ui";
<Link href="/docs">Read the documentation</Link>

<Link variant="standalone" trailingIcon={<ArrowRight />} href="/pricing">
  See pricing
</Link>

<Link external href="https://tessinaui.com">Tessina UI</Link>

When to use it

A Link navigates. It has an href, it goes somewhere, and the browser treats it as a destination — Enter follows it, middle-click opens it in a tab, a screen reader lists it among the page's links.

A Button acts. It submits, opens a dialog, saves, deletes. Space activates it, and it is announced as a button.

Do not swap one for the other to get a look. If a "link" saves a draft, it is a Button with variant="ghost"; if a "button" goes to the pricing page, it is a Button with render={<a href="/pricing" />} — or, more often, a Link.

Three more rules the reference systems agree on:

  • Write labels that stand alone. "Read the full report" beats "click here" — a screen-reader user often navigates by pulling up a list of every link on the page, with no surrounding sentence to lend meaning.
  • Keep icons off inline links. Inside a sentence they break the line rhythm and there is nothing for them to add.
  • Do not open a new tab casually. It takes control away from the reader. If you must, say so — external does that for you (see below).

Examples

Default

An inline link: brand ink, underlined at rest.

Variants

inline for links inside a sentence, standalone for links outside one, and quiet when the link should carry the surrounding ink.

Underline

Each variant derives an underline, and you can pin it explicitly.

In prose

The fine-print block and the editorial paragraph — the two places inline earns its always-on underline.

Sizes

Five sizes from xs to xl. xs is the legal and fine-print rung.

Intents

intent colours a link by meaning.

Tones

tone picks which ink the link spends. on-color reads the plate's own ink and changes the focus ring to match — put it inside a Surface.

External links

external opens a new tab, adds the rel guard, shows an icon, and — the part you cannot see — appends a hidden "(opens in a new tab)" to the accessible name.

Icons

Both slots are decorative. Use them on standalone links, not inside prose.

Standalone CTA

The "See all" / "Learn more" idiom — a section header, an empty state, an auth screen's secondary action.

Navigation lists

A footer column, where the grouping itself says "these are links" and the underline can come off.

Action links

"Change" beside a summary row. If it goes to another step it is a Link; if it edits in place it is a Button.

Visited

Opt in where a reader is choosing between many destinations.

Disabled

A disabled link leaves the tab order, loses its href, and announces itself.

Routing

render swaps the anchor for a router link and keeps everything else.

Loading

LinkSkeleton holds the link's own line box, so nothing shifts on load.

Variants

VariantDerived underlineTouch targetWhen to use
inlinealwaysnone — exempt inside textAnchors inside a sentence or paragraph — the default
standalonehover44px minimum on phonesNavigation links and CTAs sitting outside prose
quiethover44px minimum on phonesLinks that should carry the surrounding ink

variant sets a default; underline overrides it at any time.

Why inline is always underlined, and why it takes no touch floor. This palette is monochrome, so a link's ink sits close to the body ink and colour alone cannot mark it — WCAG 1.4.1. The underline is the affordance. And WCAG 2.5.5 / 2.5.8 both exempt a target inside a block of text, which is fortunate: growing an inline link would stretch the line box of the paragraph around it.

Sizes

SizeTextIconUse case
xstext-xs14pxLegal and fine print
smtext-sm16pxCompact layouts, table rows
mdtext-base16pxDefault — body copy
lgtext-lg20pxLarge CTAs, settings
xltext-xl24pxHero links, marketing

Intents

intent overrides tone for the default and muted tones. On an on-color plate the intent hue is re-lit against the plate's own ink, so it stays legible on any background.

IntentInkTypical use
noneprimary (primary-tinted-foreground in dark)Default link
errorerror / error-tinted-foreground in darkDestructive context
warningwarning-tinted-foregroundCaution
successsuccess-hoverConfirmation
infoinfoInformational

Two of these are not the token you would guess, and both are contrast decisions: raw warning measures 2.05:1 on white as text and raw success 3.14:1, so each ladder starts a rung further along. Every resting ink in this table is hard-gated at WCAG AA in both themes by pnpm audit:contrast.

Tones

ToneResting inkFocus ringUse case
defaultprimaryring-ring + 2px offsetStandard page surfaces
mutedmuted-foregroundring-ring + 2px offsetDe-emphasised links — footers, meta
on-colorcurrentColor (the plate's ink)ring-current, no offsetColoured or inverted plates

on-color changes the focus ring for a measured reason: the themed ring is brand blue, which came out around 1.1:1 against a vivid plate — well under the 3:1 a focus indicator has to clear — and its 2px offset gap paints the page backdrop, banding whatever is actually underneath.

External links

external does four things:

  • sets target="_blank" (unless you pass an explicit target)
  • sets rel="noopener noreferrer" (unless you pass an explicit rel)
  • appends the external icon
  • appends a visually-hidden "(opens in a new tab)" to the accessible name

That last one is not optional decoration. The icon renders aria-hidden — it is a cue for sighted users only — so without the hidden text a screen-reader user is moved to a new tab with no warning at all (WCAG 3.2.5, technique G201). showExternalIcon={false} hides the icon and keeps the announcement.

The rel guard and the announcement both follow where the link actually opens, not the prop, so a hand-written target="_blank" gets them too.

<Link external href="https://tessinaui.com">Tessina UI</Link>

// No icon — still announced
<Link external showExternalIcon={false} href="https://tessinaui.com">
  Tessina UI
</Link>

// Your copy already says it: don't announce it twice
<Link external hideNewTabHint href="https://tessinaui.com">
  Annual report (opens in a new tab)
</Link>

Visited

visited paints a dimmer rung of the link ink for destinations already visited. It is opt-in because it only helps where a reader is picking between many destinations — a reference list, search results, a bibliography. On a nav or a CTA that everyone clicks, it is noise.

<Link visited href="https://www.w3.org/TR/WCAG22/">WCAG 2.2</Link>

Two limits worth knowing:

  • It has no effect when intent is set or tone is not default. An error destination stays an error destination, and an on-color plate has no second ink to spend.
  • Browsers only allow colour properties on :visited, for privacy. The state cannot also change the underline or the weight — which is fine, since the underline is already carrying "this is a link".

Disabled

<Link disabled href="/billing">Billing portal</Link>

disabled mutes the link, sets aria-disabled="true", sets tabIndex={-1} so it leaves the tab order, strips the href, and turns off pointer events — on the plain anchor and through render. All of it, because any one alone is a hole: with only aria-disabled, Enter still navigates.

Prefer not needing it. HTML has no disabled anchor, and several design systems deliberately ship none. If a destination is unavailable, saying why in text beats leaving a dead link on the page.

Routing

render swaps the anchor for your router's link and keeps Tessera's styling and behaviour — the underline derivation, the focus ring, disabled, and the external handling.

import NextLink from "next/link";

<Link render={<NextLink href="/dashboard" />}>Dashboard</Link>
import { NavLink } from "react-router-dom";

<Link render={<NavLink to="/dashboard" />}>Dashboard</Link>
  • The host keeps its own destination. Put the href/to on the host and Link will not stamp over it; put it on Link and it is forwarded to the host. Pick one.
  • The host's className wins a conflict with Link's, so you can override a colour on the element you passed in.
  • The host's children are replaced by Link's content (label plus icons).

RTL

inline-flex with logical spacing, so dir="rtl" flips icon order and gaps.

<Link dir="rtl" variant="standalone" leadingIcon={<ArrowLeft />} href="#">
  عودة إلى المشاريع
</Link>

Accessibility

  • Renders a native <a>, so the role, keyboard behaviour and browser affordances are the real ones.
  • New tabs are announced. Any link resolving to target="_blank" carries a visually-hidden "(opens in a new tab)" in its accessible name.
  • rel="noopener noreferrer" is applied to every new-tab link, closing the reverse-tabnabbing hole — including through render.
  • Non-colour affordance in every variant. inline underlines at rest; standalone and quiet underline on hover and on keyboard focus.
  • Touch targets: standalone and quiet are at least 44px tall on phones, as real layout rather than an invisible overflowing pad — a pad on a stacked link list overlaps its neighbours and the wrong link wins the tap. inline is exempt per WCAG 2.5.5 / 2.5.8.
  • Focus ring is visible in both themes, and switches to the plate's own ink on tone="on-color", where the themed ring measured about 1.1:1.
  • Contrast: every resting ink is hard-gated at WCAG AA in light and dark by pnpm audit:contrast.
  • Disabled links leave the tab order and lose their href on both render paths.
  • Every part carries a data-slot: link, link-leading-icon, link-trailing-icon, link-new-tab-hint, link-skeleton, link-skeleton-leading-icon, link-skeleton-text, link-skeleton-trailing-icon.

Screen readers. VoiceOver verified in both Safari and Chrome (macOS, 2026-09-09) by capturing what it actually spoke while walking the links:

CaseVoiceOver said
External link, icon shown"link tessinaui.com (opens in a new tab)"
External link, icon hidden (showExternalIcon={false})"link No icon — still announced (opens in a new tab)"
External link with hideNewTabHint, label says it"link Annual report (opens in a new tab)" — announced once, not twice

The second row is the one that matters: the icon is aria-hidden, so with it switched off the warning survives only because it is in the accessible name. Every link announced its role, its name and the warning; inline links read in sentence order with the surrounding prose.

NVDA and JAWS are untested — no Windows host.

API Reference

Link

PropTypeDefaultDescription
variant"inline" | "standalone" | "quiet""inline"Style personality. Drives the derived underline and the touch floor.
size"xs" | "sm" | "md" | "lg" | "xl""md"Text scale, with matching icon optics.
intent"none" | "error" | "warning" | "success" | "info""none"Colour by meaning. Overrides tone for default/muted.
tone"default" | "muted" | "on-color""default"Which ink to spend. on-color also changes the focus ring.
weight"normal" | "medium" | "semibold""normal"Font weight.
underline"always" | "hover" | "none"derivedinline → always, otherwise hover. Overrides the derivation.
externalbooleanfalseNew tab + rel guard + icon + hidden new-tab hint.
showExternalIconbooleantrueHide the auto external icon. The hint stays.
hideNewTabHintbooleanfalseSuppress the hidden "(opens in a new tab)" — only when your label already says it.
visitedbooleanfalsePaint visited destinations a dimmer rung. No effect with intent or tone="on-color".
leadingIconReactNode—Decorative icon before the label (aria-hidden).
trailingIconReactNode—Decorative icon after the label. Overrides the auto external icon.
disabledbooleanfalseMute, aria-disabled, drop href, leave the tab order.
renderReactElement—Render through another element (<NextLink>, <NavLink>).
hrefstring—Destination. Forwarded to a render host when given.
targetstring—Native target. Falls back to _blank when external.
relstring—Native rel. Falls back to noopener noreferrer for any _blank link.
dir"ltr" | "rtl"—Text direction.
classNamestring—Extra classes on the root.

Extends all standard <a> attributes.

LinkSkeleton

Inline-level placeholder that occupies the real link's line box at the same size, so swapping it for the link does not reflow the line.

It is phrasing content, so it goes wherever a link goes — mid-sentence inside a <p>, a <label>, a <figcaption>. Every element it renders is a <span>.

PropTypeDefaultDescription
size"xs" | "sm" | "md" | "lg" | "xl""md"Match the link it stands in for.
charsnumber12Placeholder width in characters.
showLeadingIconbooleanfalseMirror a leadingIcon.
showTrailingIconbooleanfalseMirror a trailingIcon or the external icon.
classNamestring—Extra classes on the root.

Renders role="status" with aria-busy, and a visually-hidden "Loading". Extends all standard <span> attributes.

Line ChartMenubar

On this page

PlaygroundInstallationUsageWhen to use itExamplesDefaultVariantsUnderlineIn proseSizesIntentsTonesExternal linksIconsStandalone CTANavigation listsAction linksVisitedDisabledRoutingLoadingVariantsSizesIntentsTonesExternal linksVisitedDisabledRoutingRTLAccessibilityAPI ReferenceLinkLinkSkeleton