Skip to content

feat: add Link component to next - #4601

Merged
pomfrida merged 12 commits into
mainfrom
worktree-resilient-popping-flame
Apr 1, 2026
Merged

feat: add Link component to next#4601
pomfrida merged 12 commits into
mainfrom
worktree-resilient-popping-flame

Conversation

@pomfrida

@pomfrida pomfrida commented Mar 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add new Link component to EDS 2.0 (next)
  • Supports inline (default) and standalone variants
  • Standalone variant uses composition for icons (pass <Icon> as children)
  • Includes Figma Code Connect mapping, tests (13 passing, incl. jest-axe), and Storybook stories

Test plan

  • All 13 unit tests pass
  • Accessibility tests pass (jest-axe)
  • Lint passes
  • Visual review in Storybook (light + dark mode)

@pomfrida pomfrida self-assigned this Mar 3, 2026
@equinor equinor deleted a comment from claude Bot Mar 3, 2026
@pomfrida

pomfrida commented Mar 3, 2026

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Mar 3, 2026

Copy link
Copy Markdown

Claude finished @pomfrida's task in 2m 1s —— View job


Code Review: Link Component

  • Gather context and read changed files
  • Review component implementation
  • Review types and API design
  • Review CSS and design tokens
  • Review tests
  • Review stories and Figma Code Connect

Overall a clean, well-structured component that follows EDS 2.0 conventions. A couple of things worth addressing before merging:


Bug: Icon color mismatch in standalone default state

link.css:55-57 — the icon is hardcoded to --eds-color-text-strong:

.eds-link[data-variant='standalone'] > svg {
  color: var(--eds-color-text-strong);
}

In the default state, the link text and border-bottom are --eds-color-text-link (blue), but the icon is --eds-color-text-strong (dark neutral). These three should match. On hover everything becomes --eds-color-text-strong, so the mismatch only exists in the default state — but it's noticeable.

If the design intent is for the icon and text to always be the same colour, removing the explicit icon rule lets the icon inherit currentColor from the <a>, which means:

  • Default: icon = blue (matches text and border)
  • Hover: icon transitions to dark with the text (already handled by the parent transition: color 150ms ease-in-out)

Fix this →


Minor: Missing CSS import in Link.tsx

Icon.tsx:3 does import './icon.css'Link.tsx has no equivalent. The CSS is bundled via next/index.css (which works for package consumers), but a direct import of Link from its module path won't include styles. Following the established pattern (and the CLAUDE.md convention of importing styles last) this should be:

// Link.tsx — add as last import
import './link.css'

Fix this →


Minor: > svg selector is fragile

link.css:55.eds-link[data-variant='standalone'] > svg only matches direct SVG children. If an Icon is ever wrapped (e.g. for tooltip support), the colour rule silently stops applying. Consistent with Button's approach, but worth noting for future composition.


PR description inaccuracy

The description says "All 13 unit tests pass" — the test file has 11 tests (8 Rendering + 3 Accessibility). Minor, but worth correcting to avoid confusion.


What looks good

  • Component is minimal and clean — forwardRef, displayName, correct className merging
  • AnchorHTMLAttributes<HTMLAnchorElement> gives full native prop support with no extra wiring
  • Stories correctly use CSF3 (StoryObj) — consistent with all other next/ components
  • The ::after pseudo-element focus ring technique neatly decouples border-radius from the straight border-bottom
  • @layer eds-components usage and out-of-layer display overrides are well-commented
  • data-font-size tests at lines 85–97 cover the typography system attribute regression
  • Named exports only, all files in the right places, correct file naming convention

· Branch

@pomfrida
pomfrida force-pushed the worktree-resilient-popping-flame branch from ddc3b54 to 99756dd Compare March 3, 2026 15:52
@pomfrida

pomfrida commented Mar 4, 2026

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Mar 4, 2026

Copy link
Copy Markdown

Claude finished @pomfrida's task in 1m 57s —— View job


Code Review: Link Component

  • Gather context and read changed files
  • Review component implementation
  • Review CSS and design tokens
  • Review tests
  • Review stories and Figma Code Connect
  • Summarize findings

All items raised in the previous two reviews have been addressed. The component is in good shape — one open question on icon colour intent, and a minor consistency note.


Open question: Icon colour in default state

link.css:55-57

.eds-link[data-variant='standalone'] .eds-icon {
  color: var(--eds-color-text-strong);
}

In the default (non-hover) state, the link text and border-bottom are --eds-color-text-link (blue), while the icon is pinned to --eds-color-text-strong (dark neutral). There's no visual jump on hover because the icon stays dark throughout — but the default state has a deliberate colour split between text and icon.

This could be intentional (icon acting as a neutral anchor while the text signals the link colour), but it's worth confirming it matches the Figma design intent. If it should match, removing the rule lets the icon inherit currentColor from the <a> and transitions automatically.


Minor: CSS import pattern is inconsistent (non-blocking)

Link.tsx does not import './link.css' directly, which is consistent with Icon.tsx and Button.tsx. However TextField.tsx does import './text-field.css'. The CSS is correctly included in next/index.css so there is no functional issue — just noting the inconsistency for whenever the project settles on a single pattern.


What's been addressed since the first review

  • CSF3 storiesStoryObj used throughout, no StoryFn remnants
  • > svg selector — replaced with .eds-icon class selector, more robust under composition
  • data-font-size regression tests — lines 85–97 cover standalone and inline cases
  • startIcon/endIcon props — removed; types correctly document composition pattern

What looks good

  • Minimal, focused implementation — forwardRef, displayName, correct className merging
  • AnchorHTMLAttributes<HTMLAnchorElement> gives full native prop passthrough with no boilerplate
  • ::after pseudo-element trick for the focus ring is a clean solution that keeps border-radius independent from border-bottom
  • @layer eds-components usage and out-of-layer display overrides are well-commented in the CSS
  • 11 tests covering rendering, variant attributes, composition, ref forwarding, prop spreading, and axe accessibility
  • Named exports only (Link, LinkProps, LinkVariant), correct file structure, follows all EDS 2.0 conventions
  • next/index.css correctly includes link.css

· Branch

@equinor equinor deleted a comment from claude Bot Mar 4, 2026
/* Storybook color scheme wrapper */
.eds-storybook-wrapper {
background-color: var(--eds-color-bg-neutral-surface);
color: var(--eds-color-text-strong);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this to support dark mode in stories (for text)

@millus millus left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good! So clean and easy to read 🤩 Just some minor things here and there that me (and claude) have noted down 🤖

Comment thread packages/eds-core-react/src/components/next/Link/link.css Outdated
Comment thread packages/eds-core-react/src/components/next/Link/link.css Outdated
Comment thread packages/eds-core-react/src/components/next/Link/link.css Outdated
Comment thread packages/eds-core-react/src/components/next/Link/link.css Outdated
Comment thread packages/eds-core-react/src/components/next/Link/Link.types.ts
Comment thread packages/eds-core-react/src/components/next/Link/Link.tsx Outdated
Comment on lines +61 to +63
.eds-link {
display: inline;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can remove this one part if we make data-font-family conditional, so it will not be effective for inline. If not, just ignore this comment and keep as is.

Comment thread packages/eds-core-react/src/components/next/Link/Link.stories.tsx
Comment thread packages/eds-core-react/src/components/next/Link/link.css Outdated
Comment thread packages/eds-core-react/src/components/next/Link/Link.tsx
@pomfrida
pomfrida requested a review from millus March 6, 2026 12:01
color: var(--eds-color-text-link);
text-decoration: underline;
text-decoration-thickness: var(--eds-sizing-stroke-thin);
text-underline-offset: var(--eds-sizing-stroke-thick);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is --eds-sizing-stroke-thick used for spacing? Shouldn’t this be --eds-spacing-vertical-4xs?

border-radius: var(--eds-spacing-border-radius-rounded);
color: var(--eds-color-text-strong);
outline: var(--eds-sizing-stroke-thin) solid var(--eds-color-border-focus);
outline-offset: calc(var(--eds-sizing-stroke-thick) * 2);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn’t this be --eds-spacing-vertical-3xs?

content: '';

position: absolute;
inset: calc(var(--eds-sizing-stroke-thick) * -2);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--eds-spacing-vertical-3xs * -1?

@vnys vnys left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stroke tokens used instead of spacing tokens, apart from that it LGTM 👍

@pomfrida

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Mar 20, 2026

Copy link
Copy Markdown

Claude finished @pomfrida's task in 1m 53s —— View job


Code Review: Link Component (Round 3)

  • Gather context and read changed files
  • Review CSS — token usage (stroke vs spacing)
  • Review component API (href optionality, data-font-family)
  • Review outstanding feedback from previous reviewers
  • Summarize findings

The component has come a long way. All blocking issues from previous reviews have been addressed. Here's the current status:


@vnys' CHANGES_REQUESTED — resolved ✅

The latest commit (8abc0bb) addressed the stroke-vs-spacing token issue. Offsets now correctly use spacing tokens:

Property Token
text-underline-offset --eds-spacing-vertical-4xs
outline-offset --eds-spacing-vertical-3xs
inset (::after focus ring) calc(--eds-spacing-vertical-3xs * -1)

The remaining uses of --eds-sizing-stroke-thin are for actual stroke widths (underline thickness, border width, outline width) — those are correct.


@millus' CHANGES_REQUESTED — mostly resolved

Feedback Status
href required in types Link.types.ts:7
data-font-family conditional on standalone Link.tsx:15
data-line-height for standalone Link.tsx:17
ExternalLink story with rel="noreferrer" Link.stories.tsx:80-92
cursor: pointer — rely on browser default ✅ Not in CSS
display: inline-flex duplication ✅ Only in outside-layer block
Tests for data-font-family and data-line-height ✅ 16 tests total now

Two items still open

1. :visited styling (link.css)

There's no :visited rule. @millus noted it isn't in Figma, but it's standard browser behaviour that's useful for accessibility. Since inline links especially benefit from visited state (sighted users can track where they've been), this is worth a decision: explicitly suppress it, add a token-based colour, or leave it to the browser default. Currently the browser default applies (purple in most browsers), which may clash with the design.

Suggested options:

  • Accept browser default (document the behaviour)
  • Suppress it: .eds-link:visited { color: inherit } if the design intentionally omits it

2. Height discrepancy (Figma 15px vs browser 19px)

@millus flagged that the Figma spec shows 15px total height for the standalone link, but the rendered component measures 19px. This is likely caused by the data-font-size="md" + data-line-height="squished" combination producing a taller line box than the Figma frame. Worth investigating whether a different data-line-height value closes the gap, or whether this is a known Figma-vs-browser difference to accept.


What looks good

  • Clean, minimal implementation — 57 lines of CSS, 26 lines of TSX
  • href: string required in types is the right call for a link component
  • data-font-family now conditional — the outside-layer display: inline-flex override is still correct and necessary (typography.css sets [data-font-family] { display: block })
  • Icon inherits currentColor from the <a> — no explicit colour override, so default and hover states are consistent
  • 16 tests covering rendering, variants, composition, data attributes, ref, prop spreading, and axe (inline + standalone)
  • ExternalLink story shows the recommended target="_blank" + rel="noreferrer" pattern

The two open items above are design decisions rather than bugs — ready to merge once confirmed.
· Branch

millus
millus previously approved these changes Mar 25, 2026

@millus millus left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good 🚀

A little something from claude to note down for future work:
For consumers using React Router or Next.js will likely want an as prop (or similar) to
swap out the underlying for a router link component. Not a
blocker at all, just worth tracking.

render: () => (
<Link href="#" variant="standalone">
Standalone link
<Icon data={external_link} />

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: I think it would be more clear to have a normal arrow or some other icon here to separate the story from the external link story, so its more visually different use cases

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, but since we only have the external_link icon in Figma I think maybe this story is obsolete, so I'll just remove it (it's already covered in other stories)

millus
millus previously approved these changes Mar 31, 2026

@millus millus left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🚀

pomfrida added 11 commits April 1, 2026 09:21
- Replace hardcoded px values with design tokens for underline-offset,
  outline-offset and focus ring inset
- Remove unnecessary cursor: pointer and duplicate display: inline-flex
- Make href required in LinkProps
- Make data-font-family conditional so inline inherits from context
- Add data-line-height squished for standalone to match Figma height
- Add ExternalLink story with target="_blank" pattern
- Add tests for conditional data-font-family and data-line-height
…ink CSS

Replace --eds-sizing-stroke-thick with proper spacing tokens
(--eds-spacing-vertical-4xs, --eds-spacing-vertical-3xs) for
text-underline-offset, outline-offset, and focus ring inset.
@pomfrida
pomfrida force-pushed the worktree-resilient-popping-flame branch from b11ee42 to f910a73 Compare April 1, 2026 07:22
@pomfrida
pomfrida merged commit 05019e2 into main Apr 1, 2026
12 checks passed
@pomfrida
pomfrida deleted the worktree-resilient-popping-flame branch April 1, 2026 07:33
@github-actions github-actions Bot mentioned this pull request Apr 1, 2026
@github-actions github-actions Bot mentioned this pull request Apr 8, 2026
@github-actions github-actions Bot mentioned this pull request May 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants