feat(nav): add tooltip and cursor:pointer to notification bell icon #344
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
User story
As a user, I want the notification bell icon to show a tooltip on hover and use a pointer cursor, so that it's obviously clickable — consistent with the color-mode toggle.
Context
The color-mode toggle already has
cursor: pointerand a hover hint. The notification bell lacks both, breaking interaction consistency (Nielsen heuristic 4: Consistency and standards).Acceptance criteria
🎨 Leonie Voss — UI/UX Design Lead
Observations
NotificationBell.sveltebutton already hascursor-pointerimplicitly because<button>elements carry it by default in most browsers — but Tailwind's base reset (cursor: defaulton*) overrides this, so explicitly addingcursor-pointeris correct and necessary.ThemeToggle.svelteuses a nativetitleattribute for its hover hint. The bell should match this exactly — same mechanism, same layer.titleattribute doubles as a tooltip in all major browsers on desktop and is read by screen readers as an accessible description (supplementing the existingaria-label). It costs a single attribute.aria-labelalready carries the right text (m.notification_bell_label()→ "Benachrichtigungen"). Thetitleshould mirror the same value, so keyboard users with screen readers and mouse users with tooltips both get consistent feedback.titleshould say "3 ungelesene Benachrichtigungen" (same asaria-label), not just "Benachrichtigungen". Otherwise the tooltip contradicts the badge.p-2on ah-5 w-5icon = 20px icon + 16px padding = 36px total height. This is below the 44px minimum for the senior audience. This issue predates #344 but the spec should call it out.Recommendations
cursor-pointerto the button's class list andtitle={stream.unreadCount > 0 ? m.notification_bell_unread_label({ count: stream.unreadCount }) : m.notification_bell_label()}— bindingtitleto the same derived value asaria-labelkeeps them in sync without duplicating logic.titleattribute is exactly howThemeToggledoes it, and parity with that component is the goal.p-2top-2.5(gives 40px) or addmin-h-[44px] min-w-[44px]as a belt-and-suspenders fix. The issue title references consistency with the color-mode toggle — that toggle also usesp-1.5, so both are undersized.Open Decisions (omit if none)
cursor-pointerbe added globally tobuttonvia Tailwind's@layer baseso future buttons don't regress, or added per-component? A global rule is one change; per-component requires remembering it every time.👨💻 Felix Brandt — Fullstack Developer
Observations
<button>element inNotificationBell.svelte. No new files, no new abstractions.titleattribute value should be derived from the same expression already used foraria-label. Currentlyaria-labelis computed inline in the template. Duplicating that inline ternary fortitlewould be a copy-paste smell — extract it to a$derivedinstead.ThemeToggle.sveltehastitlehardcoded as an English string ('light mode'/'dark mode') rather than using Paraglide — that's a separate pre-existing bug, but the bell should usem.*()correctly.cursor-pointerclass belongs on the button element directly. Tailwind's preflight resetscursoron*, so<button>without explicitcursor-pointershows the default arrow in some browsers.Recommendations
$derivedat the top of<script>:aria-label={bellLabel}andtitle={bellLabel}— DRY, reactive, and correct.cursor-pointerto the button's existing class string, adjacent to the other interaction classes (hover:bg-white/10).$effectto synctitle— it is a static attribute that Svelte handles natively.cursor-pointeris present in the class list, and (b)titleattribute equals the aria-label string. This is a two-assertion component test, not an E2E test.🏗️ Markus Keller — Application Architect
Observations
<button>element. No architectural implications.NotificationBellandThemeToggle. TheThemeToggleuses a nativetitleattribute and browser-defaultcursor: pointeron<button>— except Tailwind's preflight overrides the cursor, making explicitcursor-pointernecessary in both components.cursor-pointershould be applied globally via Tailwind base styles or per-component. That is a minor style architecture decision with no downstream coupling.Recommendations
NotificationBell.svelte. Do not introduce a new wrapper component or shared utility — the change does not justify an abstraction.IconButton.sveltecomponent becomes justified (Rule of Three). Two instances do not.ThemeTogglealso lackscursor-pointer— fix both in the same commit so the inconsistency is fully resolved, not half-fixed.🔒 Nora Steiner — Application Security Engineer
Observations
cursor-pointerand atitletooltip to a button introduces no new attack surface.aria-labelon the bell button already uses i18n viam.*()— thetitleattribute should follow the same pattern. Hardcoded English strings (as seen inThemeToggle) could confuse users whose locale is German, but this is a UX issue, not a security one.titleattribute value is generated fromm.notification_bell_label()andm.notification_bell_unread_label({ count: stream.unreadCount }). Thecountvalue comes fromstream.unreadCount, which is an integer. Paraglide's message interpolation is safe — it does not evaluate user-controlled strings as HTML. No XSS vector here.Recommendations
titlemirrorsaria-labelexactly — both should use the same Paraglide keys. Do not introduce a new string that could diverge and expose unexpected information.🧪 Sara Holt — QA Engineer
Observations
titleattribute is not easily assertable in Playwright because browsers control native tooltip rendering — Playwright does not exposetitletooltip visibility as a DOM event. The correct assertion is that thetitleattribute is present with the expected value.ThemeToggleis later changed, the bell may diverge again. A shared test fixture that asserts both components have matching interaction patterns would prevent this.Recommendations
NotificationBellthat asserts:<button>element hasclasscontainingcursor-pointer.titleattribute equalsm.notification_bell_label()whenunreadCount === 0.titleattribute equalsm.notification_bell_unread_label({ count: 3 })whenunreadCount === 3.aria-labelequalstitlein both states (consistency check).titlerendering is browser chrome, not application behavior. The attribute presence test at the component level is sufficient.AxeBuilder) for the nav bar to catch any futurearia-labelregressions on the bell.Open Decisions (omit if none)
cursor-pointerviawindow.getComputedStyle(computed style, catches CSS overrides) or via the class attribute string (simpler, but would miss a CSS file removing the effect)? Computed style is more accurate but requires a real browser environment — which vitest-browser-svelte provides.⚙️ Tobias Wendt — DevOps & Platform Engineer
Observations
npm run lint && npm run check && npm run testpipeline. No additional pipeline steps required.ThemeTogglehastitleattributes with hardcoded English strings — if Paraglide is the i18n tool and the project supportsde/en/es, then hardcoded English inThemeToggleis a pre-existing inconsistency. Not a CI blocker, but worth noting.Recommendations
ThemeToggle's hardcoded'light mode'/'dark mode'strings in the same commit to keep i18n consistent across both nav buttons.📋 Elicit — Requirements Engineer
Observations
titleattribute mechanism can be verified, but visual tooltip rendering is browser-controlled.aria-labelalready handles this via Paraglide, but the AC does not explicitly state it. TheThemeToggle'stitleis hardcoded English — the spec should prevent the same mistake on the bell.Recommendations
title="Notifications"(which would pass AC2 but violate project standards).Open Decisions (omit if none)
ThemeTogglebe fixed in this issue (scope expansion) or tracked as a separate issue (scope discipline)? The current wording "matches the color-mode toggle" implicitly argues the toggle is the reference implementation — but the toggle'stitleis hardcoded English, which is already wrong. Fixing both here vs. separately is a prioritization call.🗳️ Decision Queue
Consolidated open decisions requiring human context before implementation.
Theme:
cursor-pointerscope — global vs. per-componentFrom Leonie & Markus
Should
cursor-pointerbe added globally tobuttonelements via Tailwind's@layer base(one change, prevents future regressions), or added per-component alongside other interaction classes (explicit, no global side effects)?@layer base { button { @apply cursor-pointer; } }— zero maintenance, but may surprise if a button intentionally needs a different cursor.ThemeToggleandNotificationBell.Theme:
ThemeTogglescope — same issue or separate?From Elicit, Tobias, Felix, Markus
ThemeToggle.sveltehas the samecursor-pointeromission and also has hardcoded Englishtitlestrings instead of Paraglide keys. Three options:NotificationBellandThemeTogglein this issue — keeps the "matches the toggle" consistency promise honest.ThemeTogglecursor only in this issue, track i18n strings as a separate issue.NotificationBellas scoped; open a follow-up forThemeToggleseparately.Theme: Touch target size — in scope or follow-up?
From Leonie
Both
NotificationBell(p-2) andThemeToggle(p-1.5) render touch targets below 44px. This is a WCAG 2.2 failure for the senior audience. Fix now alongside cursor/tooltip, or track separately as a dedicated accessibility issue?Global cursor
ThemeToggle can be fixed here
Touch target is descoped
Implementation complete
Branch:
feat/issue-344-bell-tooltipWhat was implemented
Commit 1 —
feat(nav): add cursor-pointer and tooltip to notification bellbellLabelas$derivedinNotificationBell.svelte— eliminates the duplicated inline ternary that was used foraria-labeland keeps tooltip/label in sync reactivelytitle={bellLabel}to the bell<button>— native tooltip matchesaria-labelin both zero and non-zero unread statescursor-pointerto the bell button's class listbutton { cursor: pointer; }rule in@layer baseoflayout.css— prevents future regressions (Decision Queue: global scope)NotificationBell.svelte.spec.ts: cursor-pointer class present, title equals aria-label when unread=0, title equals aria-label when unread=3Commit 2 —
fix(nav): replace hardcoded ThemeToggle title with Paraglide i18n keystheme_toggle_to_light/theme_toggle_to_darkkeys tode/en/esmessagesthemeLabelas$derivedinThemeToggle.svelteand bound botharia-labelandtitleto it'light mode'/'dark mode') (Decision Queue: fix ThemeToggle in this issue)Touch target size was descoped per the Decision Queue.
Test results
All 5
NotificationBelltests pass. The single pre-existing flaky timeout inDocumentList.svelte.spec.tsis unrelated and was failing onmainbefore this work.