PlTextLink
A link, in a sentence or on its own. No surface, no height and no colour unless you ask. What it has is the one mark a reader already knows means "this goes somewhere".
import { PlTextLink } from 'plass-ui';
<PlTextLink href="/pricing">the colour reference</PlTextLink>;
<PlTextLink href="https://www.w3.org/TR/WCAG22/" newTab>
WCAG 2.2
</PlTextLink>;import 'package:plass_ui/plass_ui.dart';
PlTextLink(onPressed: () => go('/pricing'), child: const Text('the colour reference'));
PlTextLink(onPressed: openWcag, external: true, child: const Text('WCAG 2.2'));Props
| Prop | Type | Default | Description |
|---|---|---|---|
| href * | string | — | Where the link goes |
| underline | 'always' | 'hover' | 'none' | 'always' | When the underline is drawn. Not a boolean, because no underline is a real choice in a nav or a footer and should have to be spelled out |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | — | Semantic colour role. There is **no** default — a link in a paragraph is usually the paragraph colour with a line under it |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | — | The type scale. Also no default — a link inside a sentence is the size of the sentence |
| newTab | boolean | false | Opens in a new tab, with the protective rel, the icon on, and a line for a screen reader |
| icon | ReactNode | boolean | — | The mark after the label. true draws the arrow when newTab is on and the chain otherwise; false draws nothing. Left out, it follows newTab |
| startIcon | ReactNode | — | A mark before the label — a favicon, a file type, a lock. Unlike icon it has no opinion: nothing is drawn unless something is put there |
| newTabLabel | string | '(opens in a new tab)' | What a screen reader hears after the label on a newTab link. Never drawn |
| render | useRender.RenderProp | — | Renders something other than an a — the Link a router brings, most of the time |
| children | ReactNode | — | The label |
| Prop | Type | Default | Description |
|---|---|---|---|
| child * | Widget | — | The label |
| onPressed | VoidCallback? | — | Called when the link is followed. Flutter has no navigation of its own, so where it goes is decided here. Leaving it out makes the link inert |
| underline | PlTextLinkUnderline | PlTextLinkUnderline.always | When the underline is drawn. Not a boolean, because no underline is a real choice in a nav or a footer and should have to be spelled out |
| colorshared | PlassColor? | null | Semantic colour role. There is **no** default — a link in a paragraph is usually the paragraph colour with a line under it |
| sizeshared | PlassSize? | null | The type scale. Also no default — a link inside a sentence is the size of the sentence |
| external | bool | false | Whether the link leaves the app. Draws the arrow, and adds a hint a screen reader reads |
| icon | Widget? | — | The mark after the label. true draws the arrow when newTab is on and the chain otherwise; false draws nothing. Left out, it follows newTab |
| startIcon | Widget? | — | A mark before the label — a favicon, a file type, a lock. Unlike icon it has no opinion: nothing is drawn unless something is put there |
| showIcon | bool? | — | Whether a mark is drawn. Left out, it follows external — which is why it is a bool? rather than a bool |
| externalLabel | String | '(opens elsewhere)' | What a screen reader hears after the label on a newTab link. Never drawn |
Every native <a> attribute passes straight through. color is excluded because it collides with the color in the table above. rel is the one thing a caller's value is merged with rather than replaced by, see below.
There is no href, and it is the one real difference: Flutter has no navigation of its own, so where a link goes is the app's to decide and onPressed is where it is decided. Leaving it out makes the link inert, which is what a link to the page you are already on should be.
What the shared axes (size color) mean across the library is in prop conventions.
Examples
underline
always is the default, and the reason is color: a link takes no colour family unless one is asked for, so with the line off there would be nothing at all distinguishing it from the sentence around it.
Hover deliberately leaves the text colour alone and only darkens the line. A link inside running prose that changes colour under the pointer drags the reader's eye off the line they were reading.
The line rests at 45% of whatever the text is and goes to the full colour under the pointer, so it works the same on an inherited colour as on an accent one.
color
Unlike every control in the library this has no default. A component that arrived pre-dyed is one a page has to undo, and a link in a paragraph is usually the paragraph's own colour with a line under it.
newTabexternal
Something changing out from under the reader is the one thing about a link that cannot be seen before it happens.
So newTab does three things at once: target="_blank", a rel that stops the new page reaching back through window.opener, and a mark. Visible as an arrow, and read out as a line a screen reader hears after the label.
rel is merged, never replaced. The common reason to write one by hand is nofollow or sponsored, which is an SEO decision; as a plain override it would silently take the protection off a link that still opens a new tab.
So external does two: it draws the arrow leaving its box, and it gives the link a hint a screen reader reads after the label. There is no target and no rel. Nothing here opens a browsing context, so there is no opener to protect against; what "leaves the app" means is the app's own business, and onPressed is where it happens.
icon
true draws the arrow leaving its box when newTab is on and the chain otherwise; false draws nothing; a node of your own replaces the glyph. Left out, it follows newTab, a link that takes over the window should say so, and a caller should have to ask for the silent version.
showIcon decides whether a mark is drawn and icon decides what it is. Left out, showIcon follows external, a link that takes the reader out of the app should say so, and a caller should have to ask for the silent version. That is the whole reason it is a bool? rather than a bool.
startIcon is the other half and the plain one: a mark before the label (a favicon, a file type, a lock) with no opinion at all. Nothing is drawn there unless something is put there, which is the difference from icon above: that one is about where the link goes and turns itself on for a link that leaves.
The glyph rides at 0.95em rather than the 1.2em an icon inside a control takes: this one sits in a sentence, and an icon as tall as the line spaces the words around it apart. Both marks are that size and both sit a quarter-em from the label, so a link wearing either still reads as a word in a paragraph.
size
Also has no default: a link inside a sentence is the size of the sentence. Set it for a link that stands on its own.
render
Takes the router's own Link while keeping the line, the mark and the focus ring.
An href on what comes back wins. A router's Link resolves an address rather than forwarding one, applying a locale prefix, a base path or a typed route, and hands the anchor the result. So an element that carries an href of its own is left alone, and only an element with none is given the href above. The other way round, every localised link in an app quietly loses its prefix: the anchor keeps the raw string, and the crawler, the middle click and the copied address all point at the wrong page.
import NextLink from 'next/link';
<PlTextLink href="/pricing" render={<NextLink href="/pricing" />}>
Pricing
</PlTextLink>;Accessibility
- Renders a real
<a href>, so it is in the browser's link list, follows on Enter, and can be opened in a new tab or copied by the reader. newTabis announced, not only drawn. The arrow says "new tab" to a reader who can see it; the visually hidden line says it to everyone else.- The underline is the primary signal, and colour is never the only one.
underline="none"is for a link whose surroundings already say what it is. - The focus ring appears on
:focus-visibleand takes a small radius, so it traces the label rather than a rectangle around the whole line box. - The component's class is doubled in the stylesheet (
.plass-link.plass-link) so a host page's.prose aor.vp-doc acannot take its colour and its line away.
- Announced as a link rather than as a button, which is what puts it in a screen reader's list of links.
- Enter and the numpad Enter follow it. Space deliberately does not: a link is not a button, and the space bar belongs to whatever is scrolling.
externalis announced, not only drawn. The arrow says "leaves the app" to a reader who can see it; the hint says it to everyone else.- The underline is the primary signal, and colour is never the only one.
PlTextLinkUnderline.noneis for a link whose surroundings already say what it is. - The focus ring only appears on what CSS calls
:focus-visible(a keyboard reaching the link, never a pointer clicking it), and takes a small radius, so it traces the label.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
href | onPressed | Flutter has no navigation of its own. Where a link goes is the app's, and this is where it is decided. |
newTab, target, rel | external | Nothing here opens a browsing context, so there is no opener to protect against. What survives is the part that matters to a reader: the mark and the announcement. |
icon as node-or-boolean | icon and showIcon | Dart has no value that is neither null nor a widget, so "draw one" and "which one" are two questions. |
render | — | Flutter has no polymorphic element. A router's own navigation is called from onPressed. |
.plass-link.plass-link | — | There is no host stylesheet to outrank. |
children | child | Flutter's name. |