Skip to content

Notification center ​

The navbar bell: an unread count, an All and Unread filter, and a list that survives a reload.

Open in demo
Open in demo

Usage ​

The component takes no props. It reads the notifications store, which is a Pinia store persisted to localStorage under sa-notifications.

features.notifications gates the bell in the navbar. See Global config.

On mount it seeds a small fixed set, and only when the list is empty, so the demo has a count to show without stacking a fresh copy on every reload. The seeded entries carry fixed ids and timestamps measured from when they were first seeded, so the relative labels still read sensibly whenever the demo is opened.

Adding notifications ​

ts
import { useNotifications } from '@shared/composables/useNotifications'

const store = useNotifications()

store.add({
  type: 'success',
  title: 'Backup finished',
  description: 'Last night’s snapshot is ready to restore from.',
  link: '/settings',
})

add takes everything except id, timestamp and read, which it sets. A link makes the row navigate when pressed.

The store ​

MemberTypeDescription
itemsNotification[]Newest first.
unreadCountnumberHow many are unread.
add(n)(n: Omit<Notification, 'id' | 'timestamp' | 'read'>) => voidAdds one at the top.
markRead(id)(id: string) => voidMarks one read.
markAllRead()() => voidMarks everything read.
remove(id)(id: string) => voidDrops one.
seedDemo()() => voidSeeds the fixed set, only when the list is empty.
$reset()() => voidEmpties the list.

Notification ​

KeyTypeDescription
idstringSet by add.
type'info' | 'success' | 'warning' | 'error'Picks the row's icon and tint.
titlestringA translation key or a literal.
descriptionstringThe same.
paramsRecord<string, string>Interpolation values, for a keyed title or description.
linkstringWhere pressing the row goes.
timestampDateSet by add. Rendered as a relative time.
readbooleanSet to false by add.

The filter ​

All and Unread are a segmented pair rather than a dropdown. There are two states, and a menu to pick between two is a click too many. The unread count rides on its own tab, so the filter doubles as the answer to "how many are left".

The filter is in-session only, and it resets when the popover closes. The store is persisted, and a saved filter would mean opening the app tomorrow to an inbox quietly hiding most of itself.

An empty list shows the empty state, with different words for "nothing at all" and "nothing unread".

The count chip ​

The badge is the shared count chip, the same shape the chat and the sidebar use, and it caps at 9+. It is not drawn in the destructive color: an unread notification is not an error, and the template keeps that color for things that have broken.

API ​

Source: src/shared/components/common/NotificationCenter.vue, src/shared/composables/useNotifications.ts

Props ​

None.

Emits ​

None.

Slots ​

None.

Accessibility ​

  • The trigger is named "Notifications", so the bell is not an unnamed icon button.
  • The All and Unread tabs carry aria-pressed, so the active filter is announced.
  • The list scrolls inside the popover at up to 320 pixels, so a long list never pushes the popover off the screen.
  • Escape closes the popover and focus returns to the bell.