Link

Navigation. The rule: if it changes the page, it is a Link; if it changes data, it is a Button.

Actions Link.jsx Link.d.ts Link.md

Playground

Change a prop and watch it. The code below follows along.

Link — Variants, sizes and states.

Install and import

Link is in @dovetail-ds/react. Install it with React, if you haven't:

npm install @dovetail-ds/react react react-dom
import { Link } from "@dovetail-ds/react";

Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.

Guidelines

Navigation. The rule: if it changes the page, it is a Link; if it changes data, it is a Button.

Use it when

  • Navigating within the app or out to another site.

Don't use it when

  • It performs an action. Use Button variant="ghost". A link that deletes something is a trap.

Example

<Link href="/settings">Account settings</Link>
<Link href="https://example.com" external>Read the spec</Link>

Variants

underline="always" is the default and is correct inside prose, because colour alone is not a sufficient signal. underline="hover" is acceptable in navigation lists where position already marks the links.

Accessibility

external adds rel="noopener noreferrer" and a visible icon, so a new tab is never a surprise.

Content

Link text describes the destination. "Account settings", not "click here" or a bare URL.

Props

The typed contract, from Link.d.ts.

import * as React from "react";

/** Navigation. If it changes the page, it is a Link; if it changes data, it is a Button. */
export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
  href: string;
  /** Opens in a new tab, adds rel="noopener noreferrer" and a visible indicator icon. */
  external?: boolean;
  /** "always" is correct for links in prose. @default "always" */
  underline?: "always" | "hover" | "never";
  /** "inherit" for links inside a coloured block where the primary colour would clash. @default "primary" */
  tone?: "primary" | "inherit";
  children?: React.ReactNode;
}

export declare function Link(props: LinkProps): React.JSX.Element;

Tokens it reads

Every custom property Link.jsx resolves, read from the source: 4 semantic. Component tier first, because that is the one to reach for.

  • semantic Shared roles. Override one and everything using that role moves with it.
TokenTierDeclared as
--dt-motion-microsemanticvar(--dt-duration-100) var(--dt-easing-standard)
--dt-space-inline-2xssemanticvar(--dt-dim-1)

Source

React only, no dependencies, every value a token.

import React from "react";

export function Link({ href, external = false, underline = "always", tone = "primary", children, style, ...rest }) {
  const [hover, setHover] = React.useState(false);
  const color = tone === "inherit" ? "inherit" : hover ? "var(--dt-text-link-hover)" : "var(--dt-text-link)";
  return (
    <a
      href={href}
      target={external ? "_blank" : undefined}
      rel={external ? "noopener noreferrer" : undefined}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      style={{
        color,
        textDecoration: underline === "never" ? "none" : underline === "hover" ? (hover ? "underline" : "none") : "underline",
        textUnderlineOffset: 2,
        textDecorationThickness: hover ? 2 : 1,
        display: external ? "inline-flex" : undefined,
        alignItems: external ? "center" : undefined,
        gap: external ? "var(--dt-space-inline-2xs)" : undefined,
        transition: "color var(--dt-motion-micro)",
        ...style,
      }}
      {...rest}
    >
      {children}
      {external && (
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
        </svg>
      )}
    </a>
  );
}

In context

Link also appears on the actions reference cards, alongside the rest of its family.