Eunomia v1.1.0
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Link

A themed anchor for inline or standalone navigation, with an optional decorative leading icon. Unlike Button acting as a link, it carries no button-like background or border styling, since it's a text link, meant to sit naturally within a sentence or a list.

Dependencies

eun-icon · if using an icon
Overview API Examples Accessibility

This library follows the open-wc recommendations for building interoperable web components.

When to use

Use link for navigation that reads as text: inline within a sentence, a "read more", an item in a footer list. As soon as the action should look and feel like a button (its own background, its own visual weight, sitting apart from body text), reach for button configured as a link instead. See Alternatives below for a side by side comparison.

Install & usage

Pick a framework in the toolbar above and these snippets adapt.

npm install @eunomia/elements
import "@eunomia/elements/link.js";
<eun-link href="https://example.com">Read the documentation</eun-link>

Importing the file registers <eun-link> as a custom element, with no further setup needed. It works with any framework, or none, since it's a standard web component.

Alternatives

You want… Prefers
An action that should look and feel like a button, with its own background and visual weight eun-button

Guidance

  • Use eun-link for navigation that reads as text: inline links within a paragraph, a "read more", a footer link list
  • Set target="_blank" when linking off-site or to a document, so the current context isn't lost
  • Add aria-label when the link text alone doesn't convey the destination out of context (a repeated "Read more" across a list of cards)
  • Using eun-link for an action that should look and feel like a button: see Alternatives above
  • Relying on the visited-state color change as the only way a user can tell a link apart from surrounding text
  • Opening internal, same-site navigation in a new tab: reserve target="_blank" for external destinations or documents

Live testing

Properties

Attributes

NameTypeDefaultDescription
visitedstyle—
aria-labelstring—An accessible label that overrides the link content. When the link also opens in a new tab, a note announcing it is appended automatically. Ignored with a slotted anchor, which takes it instead
hrefstring—The destination URL
target'_blank' | '_self' | '_parent' | '_top''_self'Where to open the linked URL. Opening in a new tab automatically adds an external link icon at the end and announces it to assistive technology, in the current locale
iconEunomiaIconName—An optional decorative icon displayed to the left of the link
theme'light' | 'dark''light'The text color theme. Use dark on a dark background, such as --eun-surface-inverse, where it takes the --eun-text-on-inverse colors
visited-style'auto' | 'off' | 'on''auto'Controls the visited state color. Auto follows the browser's real navigation history, off always renders as unvisited, and on always renders as visited
currentbooleanfalseMarks the link as the current page: it renders with aria-current="page" and keeps the --link-color-current color in every state (hover, focus, visited), for example an active item in a navigation
blockbooleanfalseMakes the link fill the width of its container, the whole row taking the hover, the focus ring and the click, even with an icon, for example an item of a vertical navigation. The label stays on one line, cut with an ellipsis when it doesn't fit

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
localestring—The locale used to resolve the "opens in a new tab" announcement. Defaults to the one set with setDefaultLocale, English otherwise
resolvedLocale—This instance's own `locale` when set, the shared default locale otherwise. Used for `Intl` formatting, so dates and numbers follow the same locale as dictionary.
dictionary—This instance's locale-resolved string dictionary — see the class doc for the resolution order.

Slots

NameDescription
(default)The link content, or a native anchor carrying the href itself when href is unset
iconReplaces the leading icon entirely with any content, not just an icon element, such as an icon from another set. Falls back to icon when empty. The icon is decorative : the focus ring surrounds the text only, unless the link is block

Events

NameTypeDescription
eunnavigateNavigationEventFired before following the link, so a client-side router can intercept the navigation. Cancelable

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--link-colorSets the color of the link
--link-color-hoverSets the color of the link on hover
--link-color-focusSets the color of the link on focus, the hover color by default
--link-focus-outline-colorSets the color of the focus ring
--link-color-visitedSets the color of a visited link
--link-color-visited-hoverSets the color of a visited link on hover
--link-color-currentSets the color of the current page link, kept on hover, focus and visited states
--link-line-heightSets the line-height of the link. Inherits the surrounding text's line-height when unset
--link-text-decorationSets the text decoration of the link, for example none, when embedding a link inside an already distinct interactive surface such as a card
--link-icon-colorSets the color of the leading icon, independently of the link text color
--link-icon-sizeSets the size of the leading icon, 18px by default. Also sets the font size of the icon slot, so an icon from another set sized in em follows it
--link-transition-durationSets the duration of the color, background and focus transitions. Defaults to --eun-duration-xl
--link-paddingSets padding around the link, for example to give its focus outline room to breathe. Defaults to 0, flush with the text
--link-padding-blockSets the vertical padding, as one length applied to both sides. Ignored when --link-padding is set
--link-padding-inlineSets the horizontal padding, as one length applied to both sides. Ignored when --link-padding is set
--link-backgroundSets a persistent background color behind the link. Defaults to transparent
--link-background-hoverSets a background color shown while the link is hovered, falling back to --link-background when unset
--link-background-focusSets a background color shown while the link is keyboard-focused, falling back to --link-background when unset
--link-border-radiusSets the corner radius of the link's own background/focus outline box

Display

In a sentence

This library follows the open-wc recommendations for building interoperable web components.

<p>
  This library follows the
  <eun-link href="https://open-wc.org" target="_blank"
    >open-wc recommendations</eun-link
  >
  for building interoperable web components.
</p>

With icon

Read the documentation
<eun-link href="https://example.com" icon="description">
  Read the documentation
</eun-link>
import "@eunomia/elements/icon.js";

The icon module must be imported alongside the link for the icon to render.

Need something other than a eun-icon, or a different icon library entirely? Slot your own content into icon instead. It replaces the prop-driven icon entirely.

Read the documentation
<eun-link href="https://example.com">
  <svg
    slot="icon"
    width="18"
    height="18"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
  >
    <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"></path>
    <path d="M14 2v6h6"></path>
  </svg>
  Read the documentation
</eun-link>

--link-icon-size sets the icon size, 18px by default. It also sets the font size of the icon slot, so an icon component from another set sized in em, as most are, follows it without targeting its svg:

<eun-link href="tel:+33400000000" style="--link-icon-size: 1.25rem">
  <span slot="icon"><PhoneIcon /></span>
  04 00 00 00 00
</eun-link>

The icon is decorative: once the link has one, the focus ring and the focus background surround the text only, with a slotted anchor too. A block link keeps them on the whole row instead.

A link on its own line

eun-link is inline-flex, so it sits on the text baseline like a word. Alone in a list item with an icon taller than the text, the baseline leaves a few pixels of descent under it. Align it to the middle, or make it a block, to remove them:

.footer-list eun-link {
  vertical-align: middle;
}

Navigation item

Set block on the links of a vertical navigation, such as a sidebar. The link fills the width of its container, and the whole row takes the click, the hover background and the focus ring, icon included. The ring stays visible when the label is visually hidden, in a sidebar collapsed to icons. The label stays on one line, cut with an ellipsis when it doesn't fit, a slotted anchor included, and with links-ssr.css before the upgrade too. Show the full label with a title or a tooltip if needed.

Dashboard Catalogue Settings
<nav class="side-nav">
  <eun-link href="/" block icon="home">Dashboard</eun-link>
  <eun-link href="/catalogue" block icon="inventory" current
    >Catalogue</eun-link
  >
  <eun-link href="/settings" block icon="settings">Settings</eun-link>
</nav>
.side-nav eun-link {
  --link-text-decoration: none;
  --link-padding: var(--eun-space-s);
  --link-background-hover: var(--eun-color-primary-alpha-10);
}

Wrapping

A link wraps like the text around it, with balanced lines. To keep one on a single line, such as a navigation item, set white-space: nowrap on eun-link: it reaches a slotted anchor too.

About our company
About our company
<!-- Wraps within its container -->
<eun-link href="/about">About our company</eun-link>

<!-- Stays on one line -->
<eun-link href="/about" style="white-space: nowrap">About our company</eun-link>

Line height

The link inherits the line-height of the text around it, so it never adds or removes vertical space inside a paragraph. Override it with --link-line-height for a standalone link that needs its own.

<eun-link href="https://example.com" style="--link-line-height: 1.2;">
  Read the documentation
</eun-link>

Dark background

Read the documentation
<eun-link href="https://example.com" theme="dark">
  Read the documentation
</eun-link>

States

Visited state

Once the browser knows the destination has already been visited, the link switches to the --eun-color-purple-700 token (--eun-color-purple-300 on theme="dark"), a wayfinding aid to help users track what they've already read. On hover, a visited link darkens further (mixing in 20% black by default). Override that separately with --link-color-visited-hover if needed.

Override the base visited color with --link-color-visited:

<eun-link href="https://example.com" style="--link-color-visited: #333;">
  Read the documentation
</eun-link>

Real :visited styling only depends on the browser's actual navigation history for href, and browsers intentionally restrict which properties can even be changed on :visited (history-sniffing prevention).

Forcing the visited state on or off

By default, visited-style is set to auto, which leaves the browser's real navigation history for href in charge, the same behavior described above. Set it to off to always render a link as unvisited regardless of that history, useful for a demo page where every destination happens to already be in the visitor's own history. Set it to on to always render as visited, for example to preview the visited look on demand.

Always unvisited Always visited
<eun-link href="https://example.com" visited-style="off">
  Always unvisited
</eun-link>
<eun-link href="https://example.com" visited-style="on">
  Always visited
</eun-link>

In React and Next.js, visitedStyle="off" is serialized by the server as a lowercase visitedstyle attribute. eun-link accepts it as an alias of visited-style, so the link doesn't flash its visited color while waiting for hydration.

Current page

Set current on the link that points at the page the visitor is on, such as the active item of a navigation. It renders aria-current="page" on the underlying anchor and colors the link with --link-color-current (the primary color by default). The color stays put on hover, focus and visited states, so the active link keeps standing out whatever the visitor does.

Home Catalogue Contact
<eun-link href="/">Home</eun-link>
<eun-link href="/catalogue" current style="--link-color-current: #dc1414;">
  Catalogue
</eun-link>
<eun-link href="/contact">Contact</eun-link>

With a router, derive current from the active route:

<eun-link href={item.href} current={pathname === item.href}>
  {item.label}
</eun-link>

Styling the focus state

eun-link:focus-visible never matches from outside, since the focusable anchor lives inside the shadow root. eun-link exposes its keyboard focus on the host as a focus-visible custom state instead, set only when the browser would show a focus ring (never after a mouse click) and cleared on blur. Target it with :state(focus-visible), which adds no attribute to the DOM.

/* An "active page" underline drawn on the host... */
.nav-link[current]::after {
  content: "";
  position: absolute;
  inset: auto 0 0;
  height: 2px;
  background: currentColor;
}

/* ...hidden while the link shows its own focus outline */
.nav-link:state(focus-visible)::after {
  opacity: 0;
}

Turbopack's CSS parser (lightningcss) doesn't recognize :state() yet, and rejects the whole file it's in, taking every other rule in that same stylesheet down with it, not just this one. Until lightningcss adds support, ship any :state(focus-visible) rule as a runtime stylesheet instead: a plain <style> tag injected from a layout or root component, or a <link> to a CSS file served as a static asset, either of which bypasses the build-time CSS pipeline entirely.

Navigation

Opening in a new tab

Setting target="_blank" automatically adds rel="noopener noreferrer" (preventing the new page from accessing window.opener, i.e. tab-napping), shows an external link icon at the end of the link, and appends "(opens in a new tab)" to the accessible name, so screen reader users aren't caught off guard. That wording follows locale, or the default set with setDefaultLocale, so a French page announces "(ouvre dans un nouvel onglet)".

Read the documentation
<eun-link href="https://example.com" target="_blank">
  Read the documentation
</eun-link>

SPA navigation

This library is framework-agnostic, so eun-link can't import next/link, Angular's RouterLink, or any other router directly. Instead, right before following href, it dispatches a cancelable eunnavigate event carrying the destination (event.href). An app-level router can listen for it and call event.preventDefault() to take over navigation itself. If nothing listens (or the listener chooses not to intercept it), the browser performs a normal native navigation. This is the same interception pattern next/link, React Router's Link, and Vue Router's router-link use internally. It's the exact same event Button dispatches for a link button, so one router bridge covers both components.

The event is skipped entirely for modified clicks (Ctrl/Cmd/Shift/Alt, middle-click) and for any target other than _self/unset, those always need the browser's native handling (new tab, new window, ...), exactly like every SPA router link does.

One router bridge, mounted once at the root of the app, covers every link on every page. The SPA navigation guide has a ready-to-use bridge for Next.js, React Router, Vue Router, Nuxt, Angular, SvelteKit and any other router, along with the check deciding which links to intercept (only same-origin paths belonging to the app, never an external URL or a mailto:/tel: URI).

Search engines and server rendering

With href on the host, the actual <a href> only exists in the shadow root, once the element has upgraded. Server-rendered HTML then carries no link and no anchor text, so crawlers that don't run JavaScript never see it. For a link that carries the site's structure (navigation, internal pages), leave href unset and place a native anchor as a direct child instead. It keeps the link's colors, its hover, focus and visited states, and current. The anchor is also what takes focus, so an aria-label goes on it. One left on eun-link is ignored in this mode, and a console warning names it.

<eun-link current>
  <a href="/guides/server-rendering/">Server-side rendering</a>
</eun-link>

In this mode, the anchor owns navigation: the link doesn't fire eunnavigate for it, so a framework's own link component (Next's <Link>, Angular's routerLink, Vue's <RouterLink>) handles routing itself. current sets aria-current="page" on the anchor. target and rel are yours to write on the anchor. href on the host remains fine for tel:, mailto: and external links. See the Server-side rendering guide for the full picture.

When the anchor opens in a new tab, the link shows the same end icon as with href, and announces the new tab through the anchor's aria-description, in the link's locale. The anchor's text and aria-label stay as you wrote them, and an aria-description of your own is kept as is.

<eun-link>
  <a href="https://open-wc.org" target="_blank" rel="noopener noreferrer">
    open-wc recommendations
  </a>
</eun-link>

The aria-description is only a fallback: it's added once the element upgrades, and some screen readers only read a description after a pause, or leave it out of their links list. To announce the new tab everywhere, pages without JavaScript included, write the wording inside the anchor with the eun-new-tab-hint class. It becomes part of the anchor's accessible name and of the server-rendered HTML, links-ssr.css hides it visually, and the link then adds no aria-description, so the new tab is never announced twice.

import Link from "next/link";

<eun-link>
  <Link href="https://open-wc.org" target="_blank" rel="noopener noreferrer">
    open-wc recommendations
    <span className="eun-new-tab-hint"> (opens in a new tab)</span>
  </Link>
</eun-link>;

The class is hidden by links-ssr.css, so load that stylesheet, as described in the Server-side rendering guide, or the wording shows up as plain text.

A global reset on a

A site stylesheet resetting every anchor, such as a { color: inherit; text-decoration: none }, also reaches the anchor slotted into eun-link. A rule from the document always wins over the link's own ::slotted(a) styles, whatever its specificity, so the anchor loses its color, its underline and its hover and visited states, and --link-color or --link-text-decoration can't bring them back. Leave these anchors out of the reset instead:

:where(a:not(eun-link > a)) {
  color: inherit;
  text-decoration: none;
}

Customization

Custom color

Set --link-color directly, also used as the hover blend base.

A visited link keeps the visited color (purple by default), even when --link-color is set. To keep your color on a visited link, either override the visited color with --link-color-visited (see "Visited state" below), or set visited-style="off".

Read the documentation
<eun-link href="https://example.com" style="--link-color: #dc1414;">
  Read the documentation
</eun-link>

The custom properties that change colors, borders, shadows and corners, at a glance. The API tab lists every property of the component.

Variable Sets
--link-color Sets the color of the link
--link-color-hover Sets the color of the link on hover
--link-color-focus Sets the color of the link on focus, the hover color by default
--link-focus-outline-color Sets the color of the focus ring
--link-color-visited Sets the color of a visited link
--link-color-visited-hover Sets the color of a visited link on hover
--link-color-current Sets the color of the current page link, kept on hover, focus and visited states
--link-icon-color Sets the color of the leading icon, independently of the link text color
--link-background Sets a persistent background color behind the link. Defaults to transparent
--link-background-hover Sets a background color shown while the link is hovered, falling back to --link-background when unset
--link-background-focus Sets a background color shown while the link is keyboard-focused, falling back to --link-background when unset
--link-border-radius Sets the corner radius of the link's own background/focus outline box

Custom icon color

The leading icon follows the link text color by default. Override it independently with --link-icon-color, which applies both to the resting and hover states.

Read the documentation
<eun-link
  href="https://example.com"
  icon="description"
  style="--link-icon-color: #dc1414;"
>
  Read the documentation
</eun-link>

Keyboard interactions

Native anchor behavior applies, nothing custom to configure:

Key Action
Tab Moves focus to the link
Enter Activates the focused link

Semantics and screen readers

href is required

Keyboard and screen reader access both come from the rendered element being a real <a href="...">: an anchor with no href is not part of the tab order and isn't exposed as a link. Always set href, since a eun-link without one looks identical but is silently unreachable by keyboard.

Aria attributes

  • aria-label: set when the link text alone doesn't convey the destination clearly out of context (e.g. a repeated "Read more" across a list of cards)
  • The leading icon, when set, is purely decorative and hidden from assistive technology automatically (handled by eun-icon itself, always rendered aria-hidden)

Visited state

The visited-state color is a wayfinding aid (helps users track what they've already read), not a WCAG requirement, but keep enough contrast against the base color if overriding it via --link-color-visited, so the distinction remains perceivable to users who do rely on it.

Current page

current renders aria-current="page" on the underlying anchor, so a screen reader announces it as the current page. Setting aria-current on the eun-link host wouldn't do that, since the anchor lives inside its shadow root. Don't rely on the --link-color-current color alone to convey the state.

Opening in a new tab

target="_blank" doesn't itself announce anything to assistive technology, so a screen reader user could otherwise be dropped into a new tab with no warning. eun-link handles this automatically: "(opens in a new tab)" is appended to the accessible name, whether it comes from aria-label or from the slotted text content (via a visually-hidden suffix in that case), with nothing to configure. With a slotted anchor, the same wording goes on the anchor's aria-description. It's announced in the link's locale, and the end icon stays decorative.

Reduced motion

The color and text-decoration-color transition on hover/focus respects prefers-reduced-motion: reduce: with that OS-level setting on, the change applies instantly instead of fading.

Reference links

  • RGAA 4.1.2
  • HTML Specification: The a element
  • MDN: Tabnabbing prevention (rel=noopener)