Help Tooltip
An info icon beside a label that opens the definition of a domain term in a tooltip, fed from one glossary map; in running text the term itself is the trigger. Built for interfaces with specialist vocabulary: heating-cost billing here, tax or payroll just as well.
Live preview
HeatingPage.svelte
Escape closes it. The dotted term inside the sentence is the fourth trigger.System efficiency is reported via the Jahresarbeitszahl and values of 3.0 or higher count as energy-efficient.
| Apartment | WMZ reading | Consumption |
|---|---|---|
| Unit 1 | 12,420 kWh | 8,150 kWh |
| Unit 2 | 9,880 kWh | 6,230 kWh |
<script lang="ts">
import { InfoCircleIcon, Input, Slider, Tooltip } from '@urbicon-ui/blocks';
// Both live beside this page; move them to $lib once more pages need them.
import HelpTooltip from './HelpTooltip.svelte';
import { glossary } from './glossary';
let consumptionShare = $state(70);
let temperature = $state(60);
</script>
<!-- Lay the blocks out in your page's own column; the width cap is the demo's. -->
<div class="w-full max-w-xl space-y-6">
<div>
<!-- The label row is hand-built so the trigger can sit in it. The span
hands its text to the slider via aria-labelledby; without that the
thumb announces itself as a bare "Slider". -->
<div class="mb-2 flex items-center gap-1">
<span id="consumption-share-label" class="text-text-primary text-sm font-medium">
Consumption share
</span>
<HelpTooltip term={glossary['heizkv-7'].term} text={glossary['heizkv-7'].text} />
<span class="text-text-tertiary ml-auto text-sm tabular-nums">{consumptionShare} %</span>
</div>
<Slider
min={0}
max={100}
step={5}
bind:value={consumptionShare}
aria-labelledby="consumption-share-label"
/>
</div>
<div>
<div class="mb-2 flex items-center gap-1">
<label for="storage-temperature" class="text-text-primary text-sm font-medium">
Storage temperature
</label>
<HelpTooltip
term={glossary.legionella.term}
text={glossary.legionella.text}
intent="warning"
/>
</div>
<Input
id="storage-temperature"
type="number"
bind:value={temperature}
placeholder="°C"
helper="Recommended: ≥ 60 °C"
/>
</div>
<!-- In running text the term itself is the trigger. It needs the tabindex to
open on keyboard focus, which the Button gives the icon triggers for free. -->
<p class="text-text-primary text-sm">
System efficiency is reported via the
<Tooltip label={glossary.jaz.text}>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<span
tabindex="0"
class="text-primary border-primary/40 inline-flex cursor-help items-baseline gap-0.5 border-b border-dotted"
>
Jahresarbeitszahl
<InfoCircleIcon class="h-3 w-3" />
</span>
</Tooltip> and values of 3.0 or higher count as energy-efficient.
</p>
<table class="text-text-primary w-full text-left text-sm">
<caption class="text-text-primary pb-3 text-left text-sm font-semibold">
Meter readings
</caption>
<thead class="border-border-hairline border-b">
<tr>
<th class="py-2 pr-4 font-medium">Apartment</th>
<th class="py-2 pr-4 font-medium">
<span class="inline-flex items-center gap-1">
WMZ reading
<HelpTooltip term={glossary.wmz.term} text={glossary.wmz.text} />
</span>
</th>
<th class="py-2 font-medium">Consumption</th>
</tr>
</thead>
<tbody class="divide-border-hairline divide-y">
<tr>
<td class="py-2 pr-4">Unit 1</td>
<td class="py-2 pr-4 tabular-nums">12,420 kWh</td>
<td class="py-2 tabular-nums">8,150 kWh</td>
</tr>
<tr>
<td class="py-2 pr-4">Unit 2</td>
<td class="py-2 pr-4 tabular-nums">9,880 kWh</td>
<td class="py-2 tabular-nums">6,230 kWh</td>
</tr>
</tbody>
</table>
</div>HelpTooltip.svelte
Button whose aria-label names the term, wrapped in the Tooltip that carries the definition.<script lang="ts">
import { Button, InfoCircleIcon, Tooltip } from '@urbicon-ui/blocks';
interface Props {
/** Definition shown in the tooltip. */
text: string;
/** The term being explained. It becomes the trigger's aria-label ("Explanation: HeizKV § 7"). */
term: string;
/** 'warning' or 'danger' for legally or safety-relevant hints. */
intent?: 'neutral' | 'warning' | 'danger';
}
let { text, term, intent = 'neutral' }: Props = $props();
</script>
<Tooltip label={text} {intent}>
<Button size="2xs" variant="ghost" aria-label="Explanation: {term}">
<InfoCircleIcon class="h-3.5 w-3.5" />
</Button>
</Tooltip>
glossary.ts
as const, so the keys stay checkable.export const glossary = {
'heizkv-7': {
term: 'HeizKV § 7',
text: 'The German Heating Costs Ordinance requires that 50% to 70% of heating costs are billed by consumption. The rest is allocated by living area.'
},
jaz: {
term: 'Jahresarbeitszahl (JAZ)',
text: 'Seasonal performance factor: heat energy delivered per kWh of electricity, averaged over the whole year. A JAZ of 3.5 turns 1 kWh of electricity into 3.5 kWh of heat.'
},
legionella: {
term: 'Legionella protection',
text: 'In central hot-water systems the storage temperature must stay at 60 °C or above to prevent legionella growth (Drinking Water Ordinance, TrinkwV § 14).'
},
wmz: {
term: 'Wärmemengenzähler (WMZ)',
text: 'Heat meter: measures the heat energy delivered, in kWh. Mandatory in multi-unit buildings, so heating and hot water can be billed to each apartment by actual use.'
}
} as const;
export type GlossaryKey = keyof typeof glossary;
Three decisions
Why the trigger is a Button
The tooltip opens on keyboard focus as well as hover, but only when the trigger can take
focus, and the icon alone says nothing to a screen reader. A Button settles both: it is focusable by nature, and
its aria-label names the term ("Explanation: HeizKV
§ 7"), which is why term is required alongside text. The inline span in the sentence pays for
skipping it with the tabindex="0" and the ignore comment
above it.
One glossary, one wording
The same term shows up in a label, a table header and a sentence; a single glossary.ts keeps the three explanations identical.
The as const map also makes a lookup on a removed key
a type error at the trigger that used it, instead of an empty tooltip. With i18n the map is
what you localise: one map per locale, same keys.
A tooltip is optional reading
The pattern fits explanations a reader can do without; anything binding belongs on the
page itself. The demo keeps that split: the tooltip explains why the 60 °C floor exists,
the floor itself stays visible in the field's helper. intent="warning" tints the panel for such hints, it does
not make them visible.