# Link

`Link` (`shared/components/Link/Link.tsx`) is the site's anchor component,
built from the BDS Link spec (`components/link.md` / `link.json` /
`link-examples.md` in `pd-xrpl-developer-docs`). It's the only anchor that
should be used for new JSX anchor tags going forward. The old design-system
link, `BdsLink`, is kept intact but unused — see
[`legacy/BdsLink.md`](/ja/shared/components/link/legacy/bdslink-1).

A live reference of every variety (size × color group × with/without icon,
light and dark) is at `/link-demo` ([link-demo.page.tsx](/ja/link-demo)),
each example labeled with the spec entry it matches.

## Usage

```tsx
import { Link } from 'shared/components/Link';
// or, in files that already import Redocly's routing Link:
import { Link as XrplLink } from 'shared/components/Link';

<Link href="/docs/concepts/" intention="brand" variation="standalone">
  Read More
</Link>

<p>
  Body copy with an <Link href="/docs/">inline link</Link> in a sentence.
</p>
```

`Link` always renders a real `<a href>` — but for anything that isn't an
external URL (`http(s):`/`mailto:`) or a bare `#fragment`, that `<a>` comes
from Redocly's own routing `Link` (`@redocly/theme/components/Link/Link`),
not a plain one. This matters, not just cosmetically: Redocly's `Link`
resolves relative paths against the current route, rewrites the path for the
current locale (so `href="/docs/concepts/"` correctly becomes
`/ja/docs/concepts/` for a `/ja/...` visitor instead of silently bouncing
them to the English page), and drives client-side navigation plus
hover-prefetch instead of a full page load. Writing `href="/docs/concepts/"`
gets all of that automatically — there's no separate "internal" prop to set.
A control that acts rather than navigates is a `Button`, even if it's meant
to look like a link.

## Props

| Prop | Values | Default | Notes |
|  --- | --- | --- | --- |
| `intention` | `brand` | `neutral` | `brand` | `brand` = XRPL green. `neutral` = black/white, no distinct hover, sage `:visited`. |
| `context` | `on-theme` | `on-inverse` | `on-saturated` | `on-theme` | Which surface the link sits on — controls both color group and focus-ring color. `neutral` + `on-saturated` is a compile-time type error (no such combination exists in the spec). |
| `variation` | `inline` | `standalone` | `inline` | `inline` sits inside running text: no fixed size (inherits the parent's font-size/line-height), no icon, `display: inline` so it wraps. `standalone` is a link acting as its own element (a card's "Read More", a CTA under a paragraph): fixed size, optional trailing icon, `display: inline-flex`. |
| `size` | `sm` | `md` | `lg` | `md` | Only affects `standalone` — `inline` always inherits from its parent. |
| `iconEnd` | `boolean` | `false` | Trailing arrow, from the shared icon set (`shared/components/Icons`) -- `XrplArrowInternalLinkIcon`, or `XrplArrowExternalLinkIcon` when `target="_blank"` is set. Only rendered when `variation="standalone"`; ignored on `inline`. |
| `href` | `string` | — | Required. |


Everything else (`target`, `rel`, `onClick`, `aria-*`, ...) passes through to
the `<a>`.

There's no `disabled` state — the old component had one, but the new spec
forbids it (ADR-XRPL-012): a disabled link is a contradiction, since a link
that can't be followed shouldn't render as a link.

## Color groups → intention × context

| `intention` | `context` | Result |
|  --- | --- | --- |
| `brand` | `on-theme` | XRPL green rest/hover, sage `:visited` |
| `brand` | `on-inverse` | Green-on-dark-surface variant (e.g. a link inside a dark card on a light page) |
| `brand` | `on-saturated` | Black-on-green (e.g. a link inside a solid-green banner) |
| `neutral` | `on-theme` | Black (light) / white (dark), flat across hover/active, sage `:visited` |
| `neutral` | `on-inverse` | Neutral tuned for a dark card on a light page |


Every resolved value lives in one place: [`styles/_link-tokens.scss`](../../../styles/_link-tokens.scss)'s
`xrpl-link-colors($group)` mixin. `Link`'s own stylesheet
([`_link.scss`](_link.scss)) just calls it per `--intention` class
(`.xrpl-link--brand`, `.xrpl-link--neutral-on-inverse`, etc.) — it never
hardcodes a color itself. Change a token once here and both `Link` and the
sitewide fallback (below) pick it up.

`neutral`'s `:hover`/`:active` are intentionally flattened to match its rest
color — that's a deliberate design decision (a black link doesn't get a color
change on hover), not an incomplete state. `:visited` is not flattened for
either group and follows the spec's sage values: `neutral` uses `$sage-11`/
`$sage-dark-11` (a muted gray, clearly distinct from rest), `brand` uses
`$sage-12`/`$sage-dark-12` (legitimately near-black/near-white). Both are
correct per spec, not bugs, and can only be seen with real browsing history —
not in an incognito/sandboxed browser, and not on `/link-demo`'s links
directly (see the reset button there, which points every demo link at a
fresh, never-visited URL instead).

## What plain `<a>` tags get (the fallback)

Most of the site's links aren't JSX — they're `[text](url)` or raw
`<a href="...">` inside `.md`/`.mdx` files. Two mechanisms make sure those
still get themed, without every markdown file needing to import a component:

1. **`MarkdownLink` override** — `@theme/components/MarkdownLink.tsx`
replaces Redocly's default renderer for markdoc's `[text](url)` syntax
(registered in `@theme/markdoc/components.tsx`).
It still renders Redocly's own routing `Link` (so client-side navigation
keeps working) but stamps it with `Link`'s class scheme via the exported
`linkClassName()` helper, forced to `intention="neutral" variation="inline"`
— docs/technical body copy stays black, not brand green, per design
direction. This covers the overwhelming majority of markdown links
site-wide with no per-file changes.
2. **CSS-cascade fallback** — [`styles/_content.scss`](../../../styles/_content.scss)'s
`a:not(.xrpl-link)` rule, scoped to `[data-component-name="Markdown/Markdown"] article`,
applies the same neutral/on-theme mixins directly. This is the safety net
for anything the `MarkdownLink` override doesn't reach: raw HTML `<a>`
embedded directly in a `.md` file (markdoc passes those through as literal
HTML, bypassing the `link` node entirely) and tag components that render
their own `<a>` (e.g. `{% child-pages %}` → `ChildPages.tsx`).


Both paths call the exact same `_link-tokens.scss` mixins `Link` itself uses,
so there's one source of truth for color/underline/focus-ring values —
nothing is hand-duplicated.

If you're building raw HTML strings that can't use JSX at all (e.g. a data
array rendered with `dangerouslySetInnerHTML`, like the FAQ answers in
[`about/index.page.tsx`](/ja/about)), you can opt a
specific link into a non-neutral treatment by writing `linkClassName()`'s
output literally into the string:

```html
<a href="..." class="xrpl-link xrpl-link--inline xrpl-link--md xrpl-link--brand xrpl-link--ctx-on-theme">...</a>
```

Anything without an `xrpl-link` class at all still falls through to the
neutral fallback above — it never renders unstyled.

## Adding a new `:not(.xrpl-link)` site

A handful of older, page/component-scoped stylesheets style `<a>` tags
directly (landing-page body copy, FAQ accordions, card descriptions, etc.),
predating this component. Each one needs a `:not(.xrpl-link)` exclusion so it
doesn't fight the component's own color rules on specificity — otherwise a
`Link` rendered inside that context can get silently overridden (this has
happened several times: `_landings.scss`, `_light-theme.scss`'s separate
`.landing` block, `_pages.scss`'s FAQ rule, `CardTextIconCard.scss`'s
description-link rule). If you add a new CSS rule that targets `a` by tag
inside some content area, exclude `.xrpl-link` from it, the same way the
existing ones do.

## Specificity gotcha: `:where()` and `bds-theme-mode`

`_link-tokens.scss`'s mixins wrap **both** light and dark branches in an
explicit `bds-theme-mode(light)` / `bds-theme-mode(dark)` call — neither is
left as an unscoped default. This isn't stylistic: an unscoped
`.xrpl-link--brand { color: ... }` is `(0,1,0)` specificity, and it loses to
`_light-theme.scss`'s generic `html.light a:where(:not(.bds-link)...)`
fallback, which resolves to `(0,1,2)` — `:where()` zeroes the specificity of
selectors *inside* it, but not `html`/`.light` sitting outside it. Wrapping
both branches the same way brings every `Link` color rule to `(0,2,1)`,
which wins cleanly without `!important`. Keep this pattern if you touch these
mixins.

## Files

- `Link.tsx` — component, types, `linkClassName()` helper
- `_link.scss` — geometry, typography, display, triggers the icon's built-in motion (see below)
- `../Icons` — `XrplArrowInternalLinkIcon` / `XrplArrowExternalLinkIcon`, the trailing arrow itself: artwork, sizing (`1em`, tracks this element's own font-size), and the hover/focus-visible motion, none of which lives in this component. See `shared/components/Icons/shared.scss` for the `bds-icon-engaged` contract and `/icon-demo` for a live reference.
- `../../../styles/_link-tokens.scss` — color/underline/focus mixins (shared with the fallback)
- `../../../@theme/components/MarkdownLink.tsx` — markdown `[text](url)` override
- `../../../styles/_content.scss` — raw-HTML-in-markdown fallback
- `../../../link-demo.page.tsx` — visual reference of every variant
- `legacy/BdsLink.tsx`, `legacy/LinkArrow.tsx` — old component, unused, kept for reference