Elements
Tooltip
A tooltip is a short hint next to what it is about: what an icon button does, the full text of a truncated label.
On the desktops and in desktop browsers it shows once the pointer has rested on its trigger for delay (500 ms), and
hides when the pointer leaves or presses the trigger. Phones and tablets show a tooltip only when the app opens it
(open). It never takes the pointer or presses, so the trigger works exactly as it does without it.
import { mount, signal, Text, View } from "nsh";
import { type OpenChangeReason, Tooltip } from "nsh/base";
import { desktop, Inspector, Log, Panel, size, Stage, ui, wide } from "./palette";
/** An icon button with a hint of the app's look, above or below it. */
function Tool(props: {
icon: string;
label: string;
hint: string;
placement: "top" | "bottom";
pinned: boolean;
onPress: () => void;
onOpenChange: (open: boolean, reason: OpenChangeReason) => void;
}) {
const hovered = signal(false);
return (
<View style={{ alignItems: "center", gap: 6 }}>
<View
onPress={props.onPress}
accessibilityLabel={props.label}
activeStyle={{ opacity: 0.6 }}
hoverStyle={{ borderColor: ui().accent }}
style={{
...ui().raised,
width: desktop ? 36 : 52,
height: desktop ? 36 : 52,
borderRadius: desktop ? 8 : 14,
alignItems: "center",
justifyContent: "center",
}}
tooltip={
<Tooltip
open={props.pinned || hovered()}
placement={props.placement}
offset={8}
margin={8}
delay={400}
onOpenChange={(open, reason) => {
hovered.set(open);
props.onOpenChange(open, reason);
}}
style={{ backgroundColor: ui().text, borderRadius: 7, paddingHorizontal: 10, paddingVertical: 6 }}
>
<Text style={{ fontSize: 13, color: ui().page }}>{props.hint}</Text>
</Tooltip>
}
>
<Text style={{ fontSize: desktop ? 17 : 22, color: ui().text }}>{props.icon}</Text>
</View>
<Text style={{ fontSize: 11, color: ui().faint }}>{props.placement}</Text>
</View>
);
}
function App() {
const events = signal<string[]>([]);
const pressed = signal("—");
const pinned = signal(false);
const log = (name: string) => (open: boolean, reason: OpenChangeReason) =>
events.set([`${name} ${open ? "shown" : "hidden"} · ${reason}`, ...events()].slice(0, 6));
const tools = () => (
<>
<Tool icon="★" label="Star" hint="Add to favorites" placement="top" pinned={pinned()} onPress={() => pressed.set("Star")} onOpenChange={log("Star")} />
<Tool icon="↗" label="Share" hint="Share with your team" placement="bottom" pinned={pinned()} onPress={() => pressed.set("Share")} onOpenChange={log("Share")} />
<Tool icon="✕" label="Delete" hint="Move to the trash" placement="top" pinned={pinned()} onPress={() => pressed.set("Delete")} onOpenChange={log("Delete")} />
</>
);
// The `tooltip` prop of any element: a text in the theme's bubble.
const pin = () => (
<View
tooltip="Shows every tooltip without a pointer"
onPress={() => pinned.set(!pinned())}
activeStyle={{ opacity: 0.6 }}
style={
wide()
? { ...ui().raised, alignSelf: "center", height: size.control, paddingHorizontal: 12, borderRadius: 8, justifyContent: "center" }
: { ...ui().raised, flex: 1, height: 40, borderRadius: 10, alignItems: "center", justifyContent: "center" }
}
>
<Text style={{ fontSize: desktop ? 13 : 14, fontWeight: "500", color: ui().text }}>
{wide() ? (pinned() ? "Unpin the tooltips" : "Pin the tooltips") : pinned() ? "Unpin" : "Pin"}
</Text>
</View>
);
const onPress = () => (
<Panel title="onPress">
<Text style={{ fontSize: 13, color: ui().text }}>{pressed()}</Text>
</Panel>
);
const changes = () => <Log title="onOpenChange(open, reason)" lines={events()} minHeight={130} />;
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>tooltip</Text>
<Text style={{ color: ui().muted }}>
{desktop ? "Rest the pointer on a button; a click still presses it" : "Tooltips follow a pointer: pin them to see them here"}
</Text>
{() =>
wide() ? (
<View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
<Stage gap={40}>
<View style={{ flexDirection: "row", justifyContent: "center", gap: 64 }}>{tools()}</View>
{pin()}
</Stage>
<Inspector>
{onPress()}
{changes()}
</Inspector>
</View>
) : (
<>
<View style={{ flex: 1, minHeight: 200, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 28 }}>{tools()}</View>
<View style={{ flexDirection: "row", gap: 8 }}>{pin()}</View>
{onPress()}
{changes()}
</>
)
}
</View>
);
}
mount(App);The quickest tooltip is the tooltip prop every element has: a text in a small bubble drawn with the theme’s popover
tokens, or an element shown as it is (Common props).
view().on_press(save).tooltip("Save the document").child(text("💾"))<View onPress={save} tooltip="Save the document">
<Text>💾</Text>
</View>Tooltip from nsh/base is the one to style, place and follow, given as the element’s
tooltip (as GPUI’s .tooltip(..)): the element is what the pointer rests on and what it is placed against. The
tooltip itself is the bubble: style it with class / style, and its children are what it says.
use nsh_base::tooltip;
use nsh_core::ui::prelude::*;
fn save_button(on_save: impl Fn() + 'static) -> impl IntoElement {
view()
.on_press(on_save)
.accessibility_label("Save")
.p_2()
.child(text("💾"))
.tooltip(
tooltip()
.placement(Placement::Bottom)
.rounded_md()
.bg(rgb(0x111827))
.px_2()
.py_1()
.child(text("Save the document").text_color(white())),
)
}import { Text, View } from "nsh";
import { Tooltip } from "nsh/base";
function SaveButton(props: { onSave: () => void }) {
return (
<View
onPress={props.onSave}
accessibilityLabel="Save"
class="p-2"
tooltip={
<Tooltip placement="bottom" class="rounded-md bg-gray-900 px-2 py-1">
<Text class="text-white">Save the document</Text>
</Tooltip>
}
>
<Text>💾</Text>
</View>
);
}It goes above its trigger by default (placement), on the other side when there is no room, and stays inside the safe
area. With open the app decides when it shows: it follows onOpenChange, which reports the pointer resting on the
trigger or leaving it, and can show it on any device.
#Props
| Prop | Rust | Type | Default | Description |
|---|---|---|---|---|
children | .child(..) | JSX.Element | none | What it says. |
class, style | .app_class(..), Styled methods | string, StyleProp<ViewStyle> | none | The look of the bubble: the tooltip itself. |
open | .open(..) | boolean | none | Shown, controlled by the app. |
defaultOpen | .default_open(..) | boolean | false | Shown at first, without open. |
delay | .delay(..) | number | 500 | Milliseconds the pointer rests on the trigger before it shows. |
disabled | .disabled(..) | boolean | false | It never shows. |
placement | .placement(..) | "top" | "bottom" | "left" | "right" | "top" | The side of the trigger it goes on. |
align | .align(..) | "start" | "center" | "end" | "center" | How it lines up with the trigger along that side. |
offset | .offset(..) | number | 4 | The gap between the trigger and the tooltip. |
margin | .margin(..) | number | 4 | The distance it keeps from the edges of the safe area. |
#Events
| Event | Rust | Description |
|---|---|---|
onOpenChange | .on_open_change(|e| ..) | The pointer rested on the trigger, or left or pressed it (reason "hover"). |
onOpen | .on_open(|| ..) | It is shown. |
onClose | .on_close(|| ..) | It is gone, also when the app hid it. |
#Accessibility
A tooltip repeats what its trigger says to screen readers: give the trigger an accessibilityLabel too, so the
hint is there for everyone.