Elemente
Link
Ein Link führt den Nutzer irgendwohin: auf eine Webseite, in eine andere App (mailto:, tel:) oder zu einem Screen der App selbst. Er besitzt das Verhalten: Presses, Return und Leertaste, solange er den Tastaturfokus hat, den deaktivierten Zustand und die Link-Rolle für Screenreader. Das Aussehen gehört der App: Ein Link hat keine eigene Farbe, keine Unterstreichung und kein eigenes Label, und seine Children sind das, was angezeigt wird. href sagt, wohin er führt; es sind Daten für das Öffnen, niemals Text auf dem Bildschirm.
import { mount, signal, Text, View } from "nsh";
import { Link } from "nsh/base";
import { desktop, focusRing, Icon, Log, Panel, size, Split, tint, ui } from "./palette";
// Links of the app's look: the accent with a quiet underline that fills in
// under the pointer, violet once visited (the app remembers what it
// opened), the palette's focus ring, dimmed while disabled. The color and
// the underline move with `transition-colors`, still with reduced motion.
const visited = signal(new Set<string>());
const events = signal<string[]>([]);
const route = signal("/docs/link");
const opened = ({ href }: { href: string }) => {
visited.set(new Set(visited()).add(href));
events.set([`onOpen ${href}`, ...events()].slice(0, 4));
};
function DocLink(props: { href: string; label: string; openWith?: "system" | "app"; disabled?: boolean; current?: boolean; icon?: boolean; inline?: boolean }) {
const seen = () => visited().has(props.href);
const color = () => (props.disabled ? ui().faint : props.current ? ui().text : seen() ? ui().violet : ui().accent);
return (
<Link
href={props.href}
openWith={props.openWith ?? "app"}
disabled={props.disabled}
current={props.current}
onOpen={(e: { href: string }) => {
opened(e);
if ((props.openWith ?? "app") === "app") route.set(e.href);
}}
class={`flex-row items-center gap-1 rounded-sm transition-colors motion-reduce:transition-none active:opacity-60 ${props.inline ? "align-text-bottom" : "self-start"}`}
focusStyle={focusRing(2)}
>
<Text
class="transition-colors motion-reduce:transition-none"
style={{
fontSize: props.inline ? 15 : size.label,
fontWeight: props.current ? "600" : "500",
color: color(),
textDecorationLine: props.current || props.disabled ? "none" : "underline",
textDecorationColor: tint(color(), 0.4),
}}
hoverStyle={props.disabled || props.current ? undefined : { textDecorationColor: color() }}
>
{props.label}
</Text>
{props.icon ? <Icon name="external" size={13} tint={color()} /> : null}
</Link>
);
}
function App() {
const crumb = (href: string, label: string) => <DocLink href={href} label={label} current={route() === href} />;
const slash = () => <Text style={{ color: ui().faint, fontSize: size.label }}>/</Text>;
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>link</Text>
<Text style={{ color: ui().muted }}>{desktop ? "Click a link, or Tab to one and press Return" : "Tap a link"}</Text>
<Split
gap={14}
inspector={
<>
<Panel title="route · visited">
<Text style={{ fontSize: 13, color: ui().text }}>
{route()} · {visited().size} visited
</Text>
</Panel>
<Log title="onOpen({ href })" lines={events()} minHeight={110} />
</>
}
>
<View style={{ ...ui().card, borderRadius: 16, padding: desktop ? 16 : 18, gap: 14 }}>
<View style={{ flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 6 }}>
{crumb("/docs", "Docs")}
{slash()}
{crumb("/docs/elements", "Elements")}
{slash()}
{crumb("/docs/link", "Link")}
</View>
<Text style={{ fontSize: 15, lineHeight: 22, color: ui().text }}>
{"Read the "}
<DocLink href="/docs/release-notes" label="release notes" inline />
{" and the "}
<DocLink href="/docs/migration" label="migration guide" inline />
{" before you update."}
</Text>
<View style={{ height: 1, backgroundColor: ui().line }} />
<View style={{ gap: 10 }}>
<DocLink href="https://example.com/" label="example.com" openWith="system" icon />
<DocLink href="/docs/archive" label="Archived docs" disabled />
</View>
</View>
</Split>
</View>
);
}
mount(App);use nsh_base::{link, theme};
use nsh_core::prelude::*;
fn docs() -> impl IntoElement {
link()
.href("https://example.com/docs")
.text_color(move || theme().primary)
.underline()
.hover(|s| s.opacity(0.8))
.focus(|s| s.rounded_sm().outline(px(2.0)).outline_color(move || theme().ring))
.child(text("Read the docs"))
}import { mount, Text } from "nsh";
import { Link } from "nsh/base";
function App() {
return (
<Link
href="https://example.com/docs"
class="self-start text-primary underline hover:opacity-80 focus:rounded-sm focus:outline-2 focus:outline-ring"
>
<Text>Read the docs</Text>
</Link>
);
}
mount(App);Link stammt aus nsh/base, dem nsh-base-Crate in Rust. In Rust erzeugt link() einen Link mit einem Setter pro Prop (.href(..), .open_with(..)), einem Listener pro Event (.on_open(..), .on_press(..)), den typisierten Style-Methoden für den Link und .child(..) für seinen Inhalt.
#Öffnen
Ein Press, ein Klick oder Return bzw. Leertaste auf dem fokussierten Link öffnet seinen href:
onOpen({ href })meldet, wohin er führt.- Mit
openWith="system"(dem Standard) wird eine URL einer anderen App oder Site, also eine mit Schema (https:,mailto:,tel:, ein von einer anderen App registriertes Schema), außerhalb der App geöffnet, so wieopenUrlsie öffnet: Zuerst übernimmt der Router der einbettenden App, danach das System. Jedes anderehref, ein Pfad der App (/orders/42) oder#anchor, wird gemeldet und bleibt bei der App. onPress()meldet den Press.
Mit openWith="app" ist der onOpen-Handler der App die Art, wie der Link sich öffnet: eine eigene Route, eine Seite in einer In-App-Web-View, zuvor eine Bestätigung.
import { Link } from "nsh/base";
<Link href={`/orders/${order.id}`} openWith="app" onOpen={({ href }) => router.push(href)} class="text-primary">
<Text>Order #{order.id}</Text>
</Link>
Das Öffnen einer URL außerhalb der App erfordert die Capability openUrl, wenn die App Capabilities in nsh.json deklariert ("openUrl": true oder { "schemes": ["https", "mailto"] }).
#Props
| Prop | Rust | Typ | Standard | Beschreibung |
|---|---|---|---|---|
href |
.href(..) |
string |
keiner | Wohin der Link führt: eine URL oder ein Pfad der App. Wird mit onOpen gemeldet, nie angezeigt. |
openWith |
.open_with(LinkOpenWith::App) |
"system" | "app" (LinkOpenWith) |
"system" |
Wer href öffnet: Das System öffnet eine URL einer anderen App oder Site; "app" überlässt es onOpen. |
disabled |
.disabled(..) |
boolean |
false |
Keine Presses, kein Press-Feedback, kein Tastaturfokus; disabled:-Styles gelten, und Screenreader sagen an, dass er abgeblendet ist. |
current |
.current(..) |
boolean |
false |
Der Link zur Seite, auf der sich der Nutzer befindet, in einer Navigation oder einem Breadcrumb: Screenreader sagen an, dass es die aktuelle Seite ist. |
accessibilityLabel |
.accessibility_label(..) |
string |
keiner | Was Screenreader vorlesen, wenn der Inhalt zu wenig sagt (ein Icon). |
hitSlop |
.hit_slop(..) |
number | Partial<Edges> |
keiner | Wie weit außerhalb seiner Grenzen Presses noch zählen, in Punkten: eine Zahl für alle Seiten oder die einzelnen Seiten benannt. Wie hitSlop einer View. |
children |
.child(..) |
JSX.Element |
keiner | Der Inhalt: ein Label, ein Icon, alles, was die App anzeigt. |
class, style |
Style-Methoden | string, StyleProp<ViewStyle> |
keiner | Das Aussehen des Links: Farbe, Unterstreichung, Padding; Texteigenschaften werden an die Texte darin weitergegeben. |
activeStyle, hoverStyle, focusStyle, disabledStyle |
.active(|s| ..), .hover(..), .focus(..), .disabled_style(..) |
StyleProp<ViewStyle> |
keiner | Der Style des Links im gedrückten, gehoverten, fokussierten oder deaktivierten Zustand, wie die <state>:-Klassen. |
#Events
| Event | Rust | Beschreibung |
|---|---|---|
onOpen |
.on_open(|href: &SharedString| ..) |
Der Link öffnet href: Mit openWith="app" öffnet der Handler ihn, mit "system" erfährt er, was das System öffnen sollte. Es kommt vor onPress. |
onPress |
.on_press(|| ..) |
Der Link wurde gedrückt: ein Tap, ein Klick oder Return bzw. Leertaste, solange er den Tastaturfokus hat. |
Ein deaktivierter Link meldet nichts, und ein Press darauf erreicht keine drückbare View darum herum.
#Zustände
Die App stylt jeden Zustand mit Klassen oder Style-Props:
<Link
href="https://example.com/changelog"
class="self-start text-primary underline decoration-primary/40 transition-colors hover:decoration-primary active:opacity-70 focus:outline-2 focus:outline-ring disabled:text-muted-foreground disabled:no-underline"
>
<Text>Changelog</Text>
</Link>
Ein besuchter Link ist Sache der App: Sie merkt sich die geöffneten hrefs und stylt diese Links über ihren eigenen State.
const visited = signal(new Set<string>());
<Link
href={href}
onOpen={({ href }) => visited.set(new Set(visited()).add(href))}
class={visited().has(href) ? "text-violet-600 underline" : "text-primary underline"}
>
<Text>{label}</Text>
</Link>
#In Text
Ein Link in einem <Text> ist eine Inline-Box des Absatzes, wie jede View in einem Text: Er sitzt in der Zeile, behält seine eigenen Zustände, seinen Fokus und seine Rolle und bleibt in einer Zeile. align-text-bottom platziert einen Link mit einer Textzeile auf der Baseline des Absatzes:
<Text class="text-[15px] text-foreground">
By continuing you accept the{" "}
<Link href="https://example.com/terms" class="align-text-bottom">
<Text class="text-[15px] text-primary underline">terms of service</Text>
</Link>
.
</Text>
Ein Link, der mit den Zeilen des Absatzes umbricht, ist ein verschachtelter <Text> mit role="link" und onPress (siehe Text), so wie TextView die Links eines Dokuments zeichnet.
#Ein kleines Ziel mit hitSlop
Ein Link kann kleiner als ein Finger sein: ein Icon, ein kurzes Wort. hitSlop lässt Presses zählen, die in der Nähe landen, in Punkten über jede Seite hinaus, ohne sein Layout oder sein Aussehen zu ändern: eine Zahl für alle Seiten oder die einzelnen Seiten benannt ({ top: 8, bottom: 8 }), so wie hitSlop einer View sie annimmt.
<Link href="https://example.com/help" hitSlop={12} accessibilityLabel="Help" class="size-5 items-center justify-center">
<Text>?</Text>
</Link>
#Barrierefreiheit
- Ein Link ist für Screenreader ein einzelnes Element mit der Rolle
"link"; sein Label ist der Text seiner Children oderaccessibilityLabel.hrefbleibt ein Datum: Screenreader sagen das Label an. currentwird als aktuelle Seite angesagt (ARIAsaria-current="page"), unddisabledso, wie die Plattform ein abgeblendetes Element ansagt.- Unter macOS und im Web ist ein Link ein Haltepunkt des Tastaturfokus: Tab erreicht ihn, Return und Leertaste öffnen ihn, und
focus:-Styles zeigen, wo der Fokus liegt.
Siehe Accessibility für Labels, Rollen und Ansagen im Allgemeinen.
#Hinweise zu Plattformen
- iOS, Android und Mac Catalyst: Ein Press ist ein Tap, der auf dem Link endet.
- Desktops (macOS, Windows, Linux, GPUI) und das Web: Ein Press ist ein Klick. Unter macOS und im Web öffnen außerdem Return und Leertaste den Link, der den Tastaturfokus hat.
- Das Web rendert einen Link als Element mit
role="link". - Jede Plattform zeichnet, was die App stylt.