Skip to content

feat(router-link): add the link states from the ionic design - #31707

Merged
thetaPC merged 15 commits into
FW-6903from
FW-6903-ionic-spec
Nov 12, 2026
Merged

thetaPC merged 15 commits into
FW-6903from
FW-6903-ionic-spec

Conversation

@thetaPC

@thetaPC thetaPC commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Issue number: resolves internal


What is the current behavior?

ion-router-link has no pressed, visited or focus styles. The browser draws its own focus ring.

What is the new behavior?

  • In the ionic theme, links match the design kit:
    • A link with no color turns #09358a when pressed or visited
    • Pressed links are underlined
    • A focused link shows a 2px #b5c0f7 ring with 4px corners
  • In the ios and md themes, a focused link shows a tinted background instead of the browser's ring, matching the clear button: the link's color at 12% opacity in md and 10% in ios, with padding that doesn't move the text
  • Adds tokens for each state: hue.<hue>.activated.color, hue.<hue>.visited.color and hue.<hue>.focus.background (also under semantic), state.focus.ring, state.focus.border.radius, state.focus.padding, and text.decoration, which state.activated and state.focus can override
  • A state token a theme leaves unset uses the link's resting value
  • Adds a states test page and screenshot test

Does this introduce a breaking change?

  • Yes
  • No
  1. Focus style: in ios and md, a focused router link now shows a tinted background instead of the browser's default ring. Customize it with the tokens for global styles, or the CSS variables for component-specific styles:
Property Token (global) CSS variable (component-specific)
Background IonRouterLink.hue.bold.focus.background --ion-router-link-hue-bold-focus-background
Background, color set IonRouterLink.hue.bold.semantic.focus.background --ion-router-link-hue-bold-semantic-focus-background
Corner radius IonRouterLink.state.focus.border.radius --ion-router-link-state-focus-border-radius
Padding IonRouterLink.state.focus.padding --ion-router-link-state-focus-padding-top, -end, -bottom, -start

Other information

Focus style in ios and md

Neither theme has a design for a focused link, and they can't keep the browser's ring, because the rule that draws the ionic ring turns it off in every theme. Both follow MD2's clear button, which shows focus as a tinted background, and reuse the button's clear focus values. A theme can set the ring, the background or both, so a future MD3 theme can show a ring through tokens alone.

Known gaps

  • Visited links aren't underlined in the ionic theme, although the design kit shows an underline. Browsers only allow color changes on :visited to protect browsing history, so text-decoration there is ignored. Visited links still get the darker color
  • In the ionic theme, links with color set keep their color when pressed or visited. The kit only shows the primary link, and the palette has no darker variant to use

How to test

The states page is new, so Before points at the basic page.

Check Before After What to look for
States, ionic Before After Focused: light blue ring. Activated: darker and underlined. Visited: darker, no underline
Focus style, md Before After Before: Tab to a link for the browser's ring. After: the Focused row has a light primary background, and red behind Danger
Focus style, ios Before After Same as md

@thetaPC
thetaPC added this pull request to stack #31706 November 6, 2026 03:47
@vercel

vercel Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ionic-framework Ready Ready Preview Nov 12, 2026 11:26pm UTC

Request Review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If we're not a fan of the focus ring, I would vote in creating a follow up PR so we can investigate what values to use by looking into MD2 and iOS native components.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Material Design 2 uses a background on focus on their clear buttons:

Image

Material Design 3 seems to use a ring for their "text" buttons:

Image

I think we should probably match MD2 for the initial MD theme that is added then support the ring when we add MD3 support. We might want to make this configurable?

I am fine with this being a follow-up ticket.

@thetaPC thetaPC Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the references! Changed md and ios to match MD2 in 78b2a24: a focused link now gets a tinted background like the clear button (the link's color at 12% in md, 10% in ios), with a bit of padding that doesn't move the text. The ring is still there for ionic, and a theme can set the ring, the background or both through tokens, so MD3 can switch to the ring without code changes.

@thetaPC
thetaPC marked this pull request as ready for review November 6, 2026 04:07
@thetaPC
thetaPC requested a review from a team as a code owner November 6, 2026 04:07
@thetaPC
thetaPC requested a review from brandyscarney November 6, 2026 04:07

@brandyscarney brandyscarney left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Left some non-blocking questions. 👍

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Material Design 2 uses a background on focus on their clear buttons:

Image

Material Design 3 seems to use a ring for their "text" buttons:

Image

I think we should probably match MD2 for the initial MD theme that is added then support the ring when we add MD3 support. We might want to make this configurable?

I am fine with this being a follow-up ticket.

Comment thread core/src/themes/ionic/default.tokens.ts Resolved
Comment thread core/src/themes/ionic/default.tokens.ts Resolved
Comment thread core/src/themes/ionic/default.tokens.ts Resolved
# Conflicts:
#	BREAKING.md
#	core/api.txt
#	core/src/components/router-link/router-link.scss

@brandyscarney brandyscarney left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@ShaneK ShaneK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me! Just a few small things, the subtle hue one is probably the main one.

hue: {
bold: {
activated: {
color: '#09358a',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since #09358a is hardcoded here and the ionic dark palette doesn't override it, a pressed or visited link would be around 1.9:1 once dark gets a real dark background. It doesn't affect anything today because ionic's dark palette is still a placeholder with a white page, so this is just something to keep in mind for FW-7558. No action required.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, I'll keep it in mind when the ionic dark palette gets its real background.

@@ -945,6 +953,35 @@ export const defaultTheme: DefaultTheme = {
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Was leaving activated and visited off subtle on purpose? Right now a pressed subtle link underlines but stays #004acd, while a bold link that looks the same at rest turns #09358a, and visited subtle links keep the resting color too. Adding the same activated and visited colors under hue.subtle makes them match.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not on purpose, thanks! Added them in df3ed27, so subtle pressed and visited links now match bold.

/**
* This behavior does not vary across directions.
*/
configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This only renders the default bold hue, but every state rule is generated for subtle too. Adding some hue="subtle" links, like chip/test/states does, would catch the subtle pressed color I mentioned on the ionic tokens. The test/states/index.html page would need them too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added subtle links to the test and to test/states/index.html in bf1b63b.

a {
@include mixins.text-inherit();

// The focus ring below replaces the browser's own ring

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
// The focus ring below replaces the browser's own ring
// The theme's focus ring or background below replaces the browser's own ring

The md and ios themes don't set any ring tokens, so the tinted background is what shows focus there. I'd also rename the should be focusable for the focus ring spec to something like should add the ion-focusable class for the same reason.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, and renamed the spec in 9bfc96b.

Comment on lines +20 to +23
* @prop --ion-router-link-state-focus-padding-bottom: Bottom padding of the focus background
* @prop --ion-router-link-state-focus-padding-end: End padding of the focus background
* @prop --ion-router-link-state-focus-padding-start: Start padding of the focus background
* @prop --ion-router-link-state-focus-padding-top: Top padding of the focus background

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* @prop --ion-router-link-state-focus-padding-bottom: Bottom padding of the focus background
* @prop --ion-router-link-state-focus-padding-end: End padding of the focus background
* @prop --ion-router-link-state-focus-padding-start: Start padding of the focus background
* @prop --ion-router-link-state-focus-padding-top: Top padding of the focus background
* @prop --ion-router-link-state-focus-padding-bottom: Bottom padding of the focus ring and background
* @prop --ion-router-link-state-focus-padding-end: End padding of the focus ring and background
* @prop --ion-router-link-state-focus-padding-start: Start padding of the focus ring and background
* @prop --ion-router-link-state-focus-padding-top: Top padding of the focus ring and background

The ionic theme has no focus background, so these only size the ring there. This matches the border-radius prop's wording.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 2e8c726.

Comment thread BREAKING.md Outdated
3. The color applied by the `color` prop is now driven by the centralized Ionic Theming system, scoped to the new `hue` property. <sup>[3](#version-10x-router-link-hue)</sup>
4. The text color now uses the color's `foreground` variant instead of `base`. <sup>[4](#version-10x-router-link-foreground)</sup>
5. Mode classes (`ion-router-link.md`, `ion-router-link.ios`) are no longer supported. <sup>[5](#version-10x-router-link-mode-classes)</sup>
5. A focused router link now shows a tinted background instead of the browser's focus ring. <sup>[5](#version-10x-router-link-focus)</sup>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
5. A focused router link now shows a tinted background instead of the browser's focus ring. <sup>[5](#version-10x-router-link-focus)</sup>
5. In the `md` and `ios` themes, a focused router link now shows a tinted background instead of the browser's focus ring. In the `ionic` theme, it shows a 2px ring. <sup>[5](#version-10x-router-link-focus)</sup>

Right now this reads like ionic gets a tinted background too, but it draws a 2px ring with no background. The main sentence in the Focus style section has the same problem.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the list item and the Focus style paragraph in 9066e4a.

test.describe(title('router-link: states'), () => {
test('should not have visual regressions', async ({ page }) => {
/**
* The padding keeps the focus ring, which sits outside the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: these comments are a lot heavier than the other router-link tests. I think the CSS ones could each be a single line, and the empty href note is already in test/states/index.html. The Firefox note is useful though, so I'd keep that one. No worries if you don't want to do that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Shortened them in e737edd and kept the Firefox note.

@thetaPC
thetaPC merged commit d25d9e4 into ionic-modular Nov 12, 2026
54 checks passed
@thetaPC
thetaPC deleted the FW-6903-ionic-spec branch November 12, 2026 23:40

This branch was successfully deployed

1 active deployment
Preview — bd5eff52 Deployed Nov 12, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: core @ionic/core package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants