Skip to main content
Urbicon UI
Back to Recipes

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.

Built with Tooltip Button Slider Input

Live preview

HeatingPage.svelte

Hover an info icon, or Tab onto it, and the definition opens; Escape closes it. The dotted term inside the sentence is the fourth trigger.
Consumption share The German Heating Costs Ordinance requires that 50% to 70% of heating costs are billed by consumption. The rest is allocated by living area. 70 %
In central hot-water systems the storage temperature must stay at 60 °C or above to prevent legionella growth (Drinking Water Ordinance, TrinkwV § 14).
Recommended: ≥ 60 °C

System efficiency is reported via the Jahresarbeitszahl 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. and values of 3.0 or higher count as energy-efficient.

Meter readings
ApartmentWMZ reading 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. Consumption
Unit 112,420 kWh8,150 kWh
Unit 29,880 kWh6,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

The shared trigger: a ghost icon 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

The map every trigger reads from; 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.