Eunomia v1.0.0-beta.2
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
aria-labelstringAn accessible label that overrides the link content. When the link also opens in a new tab, a note announcing it is appended automatically
hrefstringThe destination URL
target'_blank' | '_self' | '_parent' | '_top''_self'Where to open the linked URL. Opening in a new tab automatically announces it to assistive technology
iconEunomiaIconNameAn optional decorative icon displayed to the left of the link
theme'light' | 'dark''light'The text color theme. Use dark on a dark background
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

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

Slots

NameDescription
(default)The link content
iconReplaces the leading icon entirely with any content, not just an icon element. Falls back to icon when empty

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
--link-color-visitedSets the color of a visited link
--link-color-visited-hoverSets the color of a visited link on hover
--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-paddingSets padding around the link, for example to give its focus outline room to breathe. Defaults to 0, flush with the text
--link-backgroundSets a persistent background color behind the link. Defaults to transparent
--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

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>

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) and appends "(opens in a new tab)" to the accessible name, so screen reader users aren't caught off guard.

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

Dark background

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

Custom color

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

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

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>

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>

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.

document.addEventListener("eunnavigate", (event) => {
  // Only intercept internal routes; let external links navigate natively.
  if (!event.href.startsWith("/")) {
    return;
  }
  event.preventDefault();
  myRouter.push(event.href);
});

The framework-specific bridges below adapt to the toolbar above: pick a framework to narrow this down to just that one.

Next.js

Mount a small client-only "bridge" once, near the root layout:

"use client";

import { useRouter } from "next/navigation";
import { useEffect } from "react";

export function EunomiaRouterBridge() {
  const router = useRouter();

  useEffect(() => {
    const handleNavigate = (event) => {
      if (!event.href.startsWith("/")) {
        return;
      }
      event.preventDefault();
      router.push(event.href);
    };

    document.addEventListener("eunnavigate", handleNavigate);
    return () => document.removeEventListener("eunnavigate", handleNavigate);
  }, [router]);

  return null;
}

Angular

import { Injectable } from "@angular/core";
import { Router } from "@angular/router";

@Injectable({ providedIn: "root" })
export class EunomiaRouterBridge {
  constructor(private router: Router) {
    document.addEventListener("eunnavigate", (event) => {
      if (!event.href.startsWith("/")) {
        return;
      }
      event.preventDefault();
      this.router.navigateByUrl(event.href);
    });
  }
}

Instantiate it once (e.g. inject it in AppComponent) so its constructor registers the listener.

React Router

import { useNavigate } from "react-router-dom";
import { useEffect } from "react";

function useEunomiaRouterBridge() {
  const navigate = useNavigate();

  useEffect(() => {
    const handleNavigate = (event) => {
      event.preventDefault();
      navigate(event.href);
    };

    document.addEventListener("eunnavigate", handleNavigate);
    return () => document.removeEventListener("eunnavigate", handleNavigate);
  }, [navigate]);
}

Vue Router

import { useRouter } from "vue-router";
import { onMounted, onUnmounted } from "vue";

export function useEunomiaRouterBridge() {
  const router = useRouter();

  const handleNavigate = (event) => {
    event.preventDefault();
    router.push(event.href);
  };

  onMounted(() => document.addEventListener("eunnavigate", handleNavigate));
  onUnmounted(() =>
    document.removeEventListener("eunnavigate", handleNavigate),
  );
}

Every example above decides what to intercept, typically by checking that href is an internal, relative path. Don't intercept unconditionally: external links (a different domain, a mailto:/tel: URI, ...) must keep working as normal navigation.

Keyboard interactions

Native anchor behavior applies, nothing custom to configure:

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

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.

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.

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)