Skip to content
RTL Support: Full
Accessibility: Partial
Translations: Not Needed

Notification

Create dismissible, toast-style messages — each one rendered as an HLAlert.

Basic Usage

Notifications are created imperatively. The flow has three parts:

  1. A provider must sit above the component in the tree. HLContentWrap includes one, so in most docs examples (and apps that already wrap their tree in HLContentWrap) no extra setup is needed. Use HLNotificationProvider directly only when you need custom placement or a custom container target — see HLNotificationProvider below.
  2. Inside a component that descends from the provider, call useHLNotification() to get the notification instance.
  3. Call notification.create(), rendering your content with h(HLAlert, …). It returns the created instance, which exposes a destroy() method — capture it (as notificationInstance in the example) and call notificationInstance.destroy() from the alert's close event to dismiss that notification.
Trigger.vue
vue
<script setup lang="ts">
import { h } from 'vue'
import { HLAlert, HLButton, useHLNotification } from '@platform-ui/highrise'

// The component this runs in must descend from a notification provider
// (e.g. be wrapped in HLContentWrap).
const notification = useHLNotification()

function openNotification() {
  const notificationInstance = notification.create({
    duration: 5000,
    content: () =>
      h(
        HLAlert,
        {
          id: 'test-alert',
          title: 'Notification',
          type: 'notification',
          closable: true,
          actionOne: {
            text: 'This is a button',
            onActionClick: () => {},
          },
          actionTwo: {
            text: 'This is a button with icon',
            disabled: false,
          },
          // Dismiss this notification when the alert is closed.
          onClose: () => notificationInstance.destroy(),
        },
        {
          default: () => 'Notification description',
        }
      ),
  })
}
</script>

<template>
  <HLButton id="open-notification" @click="openNotification">Create Notification</HLButton>
</template>

Transform Legacy Notification Options

Use transformNotificationOpts when you need to map legacy /ghl-ui notification options into the HighRise notification shape.

transformNotificationOpts builds the HLAlert content for you from the legacy fields (title, description, content, meta, type, action, avatar), so you pass a flat options object instead of an h(HLAlert, …) render function. type maps to the alert's colour (success → green, warning → orange, error → red, info → blue), and duration drives both the notification's dismiss timer and the alert's own auto-close countdown.

Trigger.vue
vue
<script setup lang="ts">
import { h, onBeforeUnmount } from 'vue'
import { HLButton, transformNotificationOpts, useHLNotification } from '@platform-ui/highrise'

const notification = useHLNotification()
let notificationInstance: { destroy: () => void } | null = null

function openNotificationTransformed() {
  notificationInstance = notification.create(
    transformNotificationOpts({
      title: 'Custom object successfully updated',
      duration: 2000,
      type: 'success',
      description: 'This is a description',
      content: 'This is a content',
      meta: 'This is a meta',
      action: () =>
        h(HLButton, { id: 'dismiss-button', onClick: () => notificationInstance?.destroy() }, { default: () => 'Dismiss' }),
    })
  )
}

// Clear any notifications still on screen when this component unmounts.
onBeforeUnmount(() => notification.destroyAll())
</script>

<template>
  <HLButton id="open-notification-transformed" @click="openNotificationTransformed">Create Notification</HLButton>
</template>

Sharing one notification instance across an app

useHLNotification() must run inside a component that descends from HLNotificationProvider. But in a real app you usually want to fire notifications from many places — stores, composables, deeply-nested components — where you can't (or don't want to) call useHLNotification() again.

The recommended pattern is to call useHLNotification() once in a root component (inside the provider), stash the instance in a small module singleton, and expose typed helpers (createSuccessNotification, createErrorNotification, …) that render an HLAlert for you. The helpers build the HLAlert content — including auto-destroy on close — so callers only pass a title and message.

ts
import { HLAlert, useHLNotification } from '@platform-ui/highrise'
import type { HLAlertColor } from '@platform-ui/highrise'
import { h } from 'vue'

type NotificationApi = ReturnType<typeof useHLNotification>

let notificationInstance: NotificationApi | null = null

/**
 * Call once from a root component that descends from `HLNotificationProvider`
 * (see `Root.vue` below), passing the instance returned by `useHLNotification()`.
 * Every helper below then routes through that single instance.
 */
export const initNotification = (notification: NotificationApi) => {
  notificationInstance = notification
}

export const clearNotification = () => {
  notificationInstance = null
}

const getNotification = () => {
  if (!notificationInstance) {
    console.warn('Notification instance is not ready yet. Did you call initNotification() in your root component?')
    return null
  }
  return notificationInstance
}

let uid = 0

export interface NotifyOptions {
  title: string
  message?: string
  /** Auto-dismiss after N ms. Pass `0` to keep it until closed manually. Defaults to 3000. */
  duration?: number
}

const notify = (color: HLAlertColor, { title, message, duration = 3000 }: NotifyOptions) => {
  const notification = getNotification()
  if (!notification) return null

  const id = `hr-notification-${uid++}`
  const instance = notification.create({
    duration,
    content: () =>
      h(
        HLAlert,
        {
          id,
          title,
          color,
          type: 'notification',
          closable: true,
          onClose: () => instance?.destroy(),
        },
        { default: () => message }
      ),
  })
  return instance
}

export const createSuccessNotification = (options: NotifyOptions) => notify('green', options)
export const createErrorNotification = (options: NotifyOptions) => notify('red', options)
export const createWarningNotification = (options: NotifyOptions) => notify('orange', options)
export const createInfoNotification = (options: NotifyOptions) => notify('blue', options)
vue
<script setup lang="ts">
// Mounted once, inside HLNotificationProvider.
import { onBeforeUnmount } from 'vue'
import { useHLNotification } from '@platform-ui/highrise'
import { clearNotification, initNotification } from './notificationUtils'

// Call useHLNotification() directly in setup — it injects from the provider,
// so it must not be deferred into onMounted or an event handler.
initNotification(useHLNotification())

onBeforeUnmount(() => {
  clearNotification()
})
</script>
ts
// Any module, store, or component — no provider context needed.
import { createSuccessNotification } from './notificationUtils'

createSuccessNotification({
  title: 'Saved',
  message: 'Your changes were saved.',
})

Placement

The placement prop accepts top, bottom, top-left, top-right, bottom-left, and bottom-right. It defaults to top-right, so omitting it entirely gives you the same result as the top-right trigger below.

vue
<script setup lang="ts">
const placementOptions = ['top-left', 'top', 'top-right', 'bottom-left', 'bottom', 'bottom-right'] as const
</script>

<template>
  <div class="grid p-4" style="grid-template-rows: repeat(2, 1fr); grid-template-columns: repeat(3, 1fr); gap: 10px;">
    <HLNotificationProvider v-for="placement in placementOptions" :key="placement" :placement="placement">
      <NotificationTrigger
        :id="'placement-' + placement"
        :title="placement + ' title'"
        :description="placement + ' description'"
        :duration="1000"
      >
        {{ placement }}
      </NotificationTrigger>
    </HLNotificationProvider>
  </div>
</template>

<!-- Omitting `placement` altogether is the same as placement="top-right" -->
vue
<script setup lang="ts">
import { h } from 'vue'
import { HLAlert, HLButton, useHLNotification } from '@platform-ui/highrise'

const props = withDefaults(
  defineProps<{
    id: string
    title?: string
    description?: string
    duration?: number
  }>(),
  {
    title: 'Notification',
    description: 'Notification description',
    duration: 5000,
  }
)

const notification = useHLNotification()
let notificationInstance: { destroy: () => void } | null = null

const handleCreateNotification = () => {
  notificationInstance = notification.create({
    content: () =>
      h(
        HLAlert,
        {
          title: props.title,
          closable: true,
          id: props.id,
          type: 'notification',
          color: 'green',
          onClose: () => {
            notificationInstance?.destroy()
          },
        },
        {
          default: () => props.description,
        }
      ),
    duration: props.duration,
  })
}
</script>

<template>
  <HLButton :id="id" @click="handleCreateNotification">
    <slot>Create Notification</slot>
  </HLButton>
</template>

Max Notifications

The max prop caps the number of notifications shown at once, queuing any beyond the limit.

vue
<template>
  <HLNotificationProvider placement="top-right" :max="3">
    <NotificationTrigger id="notification-provider-max-trigger" :duration="0">
      Fire Notifications
    </NotificationTrigger>
  </HLNotificationProvider>
</template>
vue
<script setup lang="ts">
import { h } from 'vue'
import { HLAlert, HLButton, useHLNotification } from '@platform-ui/highrise'

const props = withDefaults(
  defineProps<{
    id: string
    title?: string
    description?: string
    duration?: number
  }>(),
  {
    title: 'Notification',
    description: 'Notification description',
    duration: 5000,
  }
)

const notification = useHLNotification()
let notificationInstance: { destroy: () => void } | null = null

const handleCreateNotification = () => {
  notificationInstance = notification.create({
    content: () =>
      h(
        HLAlert,
        {
          title: props.title,
          closable: true,
          id: props.id,
          type: 'notification',
          color: 'green',
          onClose: () => {
            notificationInstance?.destroy()
          },
        },
        {
          default: () => props.description,
        }
      ),
    duration: props.duration,
  })
}
</script>

<template>
  <HLButton :id="id" @click="handleCreateNotification">
    <slot>Create Notification</slot>
  </HLButton>
</template>

Teleport Target

The to prop teleports notifications into a specific DOM element instead of the document body.

Custom teleport target
vue
<template>
  <HLNotificationProvider to="#notification-teleport-target" :container-style="{ position: 'absolute' }" :max="2">
    <div class="space-y-3">
      <NotificationTrigger id="notification-provider-teleport-trigger" :duration="50000">
        Create Teleported Notification
      </NotificationTrigger>
      <div
        id="notification-teleport-target"
        style="position: relative; min-height: 180px; border: 1px dashed #98A2B3; border-radius: 8px; padding: 16px; overflow: hidden;"
      >
        Custom teleport target
      </div>
    </div>
  </HLNotificationProvider>
</template>
vue
<script setup lang="ts">
import { h } from 'vue'
import { HLAlert, HLButton, useHLNotification } from '@platform-ui/highrise'

const props = withDefaults(
  defineProps<{
    id: string
    title?: string
    description?: string
    duration?: number
  }>(),
  {
    title: 'Notification',
    description: 'Notification description',
    duration: 5000,
  }
)

const notification = useHLNotification()
let notificationInstance: { destroy: () => void } | null = null

const handleCreateNotification = () => {
  notificationInstance = notification.create({
    content: () =>
      h(
        HLAlert,
        {
          title: props.title,
          closable: true,
          id: props.id,
          type: 'notification',
          color: 'green',
          onClose: () => {
            notificationInstance?.destroy()
          },
        },
        {
          default: () => props.description,
        }
      ),
    duration: props.duration,
  })
}
</script>

<template>
  <HLButton :id="id" @click="handleCreateNotification">
    <slot>Create Notification</slot>
  </HLButton>
</template>

Notification Lifecycle Events

create() accepts two lifecycle callbacks alongside content and duration:

  • onAfterEnter — the notification has finished its enter transition.
  • onAfterLeave — it has fully left and been removed from the DOM.

Use onAfterLeave for cleanup that must not run while the notification is still animating out, such as releasing a queued item or firing the next step in a sequence.

Event Log:

No events yet. Create a notification and let it auto-dismiss.
NotificationLifecycleTrigger.vue
vue
<script setup lang="ts">
import { h } from 'vue'
import { HLAlert, HLButton, useHLNotification } from '@platform-ui/highrise'

const notification = useHLNotification()
let notificationInstance: { destroy: () => void } | null = null

const handleCreateNotification = () => {
  notificationInstance = notification.create({
    duration: 3000,
    onAfterEnter: () => console.log('onAfterEnter — finished entering'),
    onAfterLeave: () => console.log('onAfterLeave — removed from the DOM'),
    content: () =>
      h(
        HLAlert,
        {
          id: 'lifecycle-alert',
          title: 'Lifecycle',
          closable: true,
          type: 'notification',
          color: 'green',
          onClose: () => notificationInstance?.destroy(),
        },
        { default: () => 'Auto-dismisses after 3000ms, or close it yourself.' }
      ),
  })
}
</script>

<template>
  <HLButton id="notification-lifecycle-trigger" @click="handleCreateNotification">Create Notification</HLButton>
</template>

Imports

ts
import { HLAlert, HLNotificationProvider, transformNotificationOpts, useHLNotification } from '@platform-ui/highrise'
import type { HLNotificationOptions, HLNotificationProviderProps } from '@platform-ui/highrise'

Props

HLNotificationProvider Props

PropTypeDefaultDescription
placement'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right''top-right'Where notifications appear on screen.
maxnumberundefinedMaximum number of notifications shown at once. Any beyond the limit are queued. Uncapped when omitted.
tostring | HTMLElementundefinedTarget element (or selector) to teleport the notification container into. Defaults to document.body.
container-classstringundefinedClass applied to the notification container.
container-stylestring | CSSPropertiesundefinedInline style applied to the notification container.
scrollablebooleantrueAllow the container to scroll when notifications overflow. Ignored for top and bottom placements.
keep-alive-on-hoverbooleanfalsePause a notification's auto-close timer while the pointer is over it. Only applies to notifications created with a duration; can also be set per notification via create().

Slots

HLNotificationProvider Slots

NameParametersDescription
default()The default slot.

Notification API

Call useHLNotification() inside a component that descends from a provider (directly, or via HLContentWrap). It returns an instance with two methods:

MethodSignatureDescription
create(options: HLNotificationOptions)Shows a notification and returns a reference to it (see below).
destroyAll() => voidImmediately removes every notification created through this instance.

create options

HLNotificationOptions carries the notification-shell settings. The visible content — title, description, colour, close button, actions — comes from the HLAlert you render inside content, so those live on HLAlert, not here. See the Alert props for that surface.

OptionTypeDefaultDescription
content() => VNodeChildundefinedRender function for the notification body. Return an HLAlert (via h) to get the standard visual.
durationnumberundefinedAuto-dismiss after this many milliseconds. Omit (or 0) to keep it until dismissed manually.
keepAliveOnHoverbooleanfalsePause this notification's auto-close timer while the pointer is over it. Has no effect without a duration.
onAfterEnter() => voidundefinedCalled once the notification has finished its enter transition.
onAfterLeave() => voidundefinedCalled once the notification has finished its leave transition and is removed from the DOM.
onMouseenter(e: MouseEvent) => voidundefinedCalled when the pointer enters the notification.
onMouseleave(e: MouseEvent) => voidundefinedCalled when the pointer leaves the notification.

INFO

The notification shell's own close button is always suppressed — create() forces closable: false on it — so the visible close control comes from the HLAlert you render in content. Keep closable: true on that alert and wire its close event to destroy().

The create return value

create() returns a reference to the notification it created:

MemberSignatureDescription
destroy() => voidDismisses this specific notification.
keystringThe unique key assigned to this notification.

A common pattern is to keep the returned reference and call destroy() from the HLAlert's close event so the notification is removed when the user dismisses the alert:

ts
const instance = notification.create({
  duration: 5000,
  content: () =>
    h(HLAlert, { id: 'saved', title: 'Saved', onClose: () => instance.destroy() }, { default: () => 'Your changes were saved.' }),
})

Accessibility

The notification's accessibility comes from the HLAlert you render inside it:

  • HLAlert renders with role="alert" by default, which maps to aria-live="assertive" so assistive technology announces it as soon as it appears. Set the alert's role to status for non-urgent messages, which announces politely (aria-live="polite") without interrupting.
  • Keep closable: true (the default) so the alert renders a keyboard-focusable close control; wire its close event to the notification's destroy() so dismissing it also removes the notification.
  • An auto-closing HLAlert pauses its own timer while it is focused or hovered and resumes on blur/leave, so keyboard users who tab into a notification are not raced by the timeout.
  • Provide an ariaLabel on any icon-only action button (actionOne / actionTwo) so its purpose is announced.