vue-toast-kit — система уведомлений для Vue 3 без компромиссов

26.05.2026
vue-toast-kit — система уведомлений для Vue 3 без компромиссов

Toast-уведомления нужны в каждом Vue-приложении. Два варианта: взять vue-toastification (последний релиз три года назад, API устарел) или vue-sonner (красиво, но минималистично — Promise API есть, а дальше сам). Третий вариант — написать свой.

vue-toast-kit — пакет с полным набором фич для Vue 3 и Nuxt 3. Promise API с автопереключением типа, приоритетная очередь с вытеснением, undo с прогресс-таймером, группировка тостов, headless режим, design system из 30+ CSS custom properties, stack mode как в Sonner, event emitter для аналитики, rate limiting, localStorage persist. 9.2 KB gzip, одна peer-зависимость — Vue 3.

Установка

bash Copy
npm install vue-toast-kit
ts Copy
// main.ts
import { createApp } from 'vue'
import { VueToastPlugin } from 'vue-toast-kit'
import 'vue-toast-kit/style'
import App from './App.vue'

const app = createApp(App)
app.use(VueToastPlugin, { position: 'bottom-right', theme: 'system' })
app.mount('#app')
vue Copy
<!-- App.vue — один контейнер на всё приложение -->
<template>
  <RouterView />
  <ToastContainer />
</template>

<ToastContainer> регистрируется глобально плагином — импортировать не нужно.


Базовый API

useToast() работает внутри компонентов. Именованный синглтон toast — вне компонентов: в Pinia-сторах, axios-интерцепторах, роутере.

ts Copy
import { useToast } from 'vue-toast-kit'

const toast = useToast()

toast.success('Файл загружен')
toast.error('Соединение прервано')
toast.warning('Осталось 5 МБ из 10 МБ')
toast.info('Обновление доступно')
toast.loading('Подключение к серверу…')  // sticky, без автозакрытия
ts Copy
// Вне компонента — в Pinia store, axios interceptor и т.д.
import { toast } from 'vue-toast-kit'

axios.interceptors.response.use(null, (err) => {
  toast.error(`Ошибка: ${err.message}`)
  return Promise.reject(err)
})

Каждый вызов возвращает id. Через него можно обновить или закрыть тост позже.

ts Copy
const id = toast.loading('Обработка…')
// ...
toast.update(id, { type: 'success', message: 'Готово!' })
// Или только текст, без изменения опций:
toast.updateMessage(id, 'Почти готово…')
// Закрыть программно:
toast.dismiss(id)

Promise API

Самое частое применение loading-тоста — ждать промис. toast.promise делает это за вас: показывает loading, переключает в success или error по результату, возвращает исходный промис нетронутым.

ts Copy
const user = await toast.promise(
  fetchUser(id),
  {
    loading: 'Загружаем профиль…',
    success: (u) => `Добро пожаловать, ${u.name}!`,
    error:   (e) => `Не удалось загрузить: ${(e as Error).message}`,
  },
)

Функции success и error получают данные и ошибку — можно строить динамические сообщения. Reject пробрасывается дальше, try/catch в вашем коде работает как обычно.

В Pinia store:

ts Copy
// stores/files.ts
import { toast } from 'vue-toast-kit'

export const useFileStore = defineStore('files', {
  actions: {
    async upload(file: File) {
      return toast.promise(
        uploadAPI(file),
        {
          loading: `Загружаем ${file.name}…`,
          success: (res) => `${res.name} загружен (${res.size} KB)`,
          error:   (e)   => `Ошибка: ${(e as Error).message}`,
        },
      )
    },
  },
})

Undo с прогресс-таймером

Пользователь удалил запись — нужно дать несколько секунд на отмену. toast.undo рисует прогресс-бар внутри тоста. Клик по кнопке — вызывается onUndo и тост закрывается. Таймер вышел — тост закрывается молча, onAutoClose для финальной операции.

ts Copy
function deleteFile(id: string) {
  markForDeletion(id)

  toast.undo(`Файл "${fileName}" удалён`, {
    undo: {
      label:    'Восстановить',
      duration: 6000,
      onUndo:   () => {
        restoreFile(id)
        toast.success('Файл восстановлен')
      },
    },
    onAutoClose: () => permanentlyDelete(id),
  })
}

onUndo может быть async — ошибки внутри не сломают тост:

ts Copy
toast.undo('Письмо заархивировано', {
  undo: {
    onUndo: async () => {
      await api.restore(emailId)
      toast.success('Письмо возвращено во входящие')
    },
  },
})

Приоритетная очередь

Четыре уровня: critical, high, normal, low. Когда слоты заполнены и приходит тост с высоким приоритетом — он вытесняет тост с низким. Вытесненный переходит в pending-очередь и всплывает когда место освободится.

ts Copy
// Слоты заполнены нормальными тостами
toast.error('Критическая ошибка сервера', { priority: 'critical' })
// Вытолкнет самый низкоприоритетный тост в pending, встанет сам

Pending-очередь отсортирована по приоритету. Освобождается слот — первым встаёт самый важный.

ts Copy
// Для некритичных уведомлений
toast.info('Новое сообщение от Алексея', { priority: 'low' })
toast.success('Автосохранение')

// Эти не будут вытеснять другие, просто встанут в очередь

maxVisible задаётся на <ToastContainer>:

vue Copy
<ToastContainer :max-visible="3" position="bottom-right" />

Группировка

Несколько тостов с одним groupKey складываются в стопку с каунтером +N. Клик по каунтеру — разворачивает группу, показывает все тосты. Закрытие лидера — следующий тост автоматически становится лидером.

ts Copy
// Три вызова — один видимый тост "+2"
toast.info('Новое сообщение от Алисы',  { groupKey: 'messages' })
toast.info('Новое сообщение от Бориса', { groupKey: 'messages' })
toast.info('Новое сообщение от Карины', { groupKey: 'messages' })

Удобно для нотификаций о системных событиях, которые могут массово прийти за короткое время.


Stack mode (Sonner-style)

stackMode складывает тосты в визуальный стек — видна только верхняя карточка, за ней угадываются до двух следующих, слегка уменьшенных и сдвинутых. Наведение мыши — стек разворачивается в обычный список.

vue Copy
<ToastContainer :stack-mode="true" position="bottom-right" />

Полезно когда нотификации появляются часто и не должны занимать много места на экране.


Design System

30+ CSS custom properties с тремя готовыми темами.

vue Copy
<!-- Тема через prop -->
<ToastContainer theme="dark" />
<ToastContainer theme="system" />  <!-- следует prefers-color-scheme -->

Токены можно переопределить глобально в :root или передать объект прямо в prop:

vue Copy
<ToastContainer
  :theme="{
    colorBg:      '#1a1a2e',
    colorText:    '#e2e8f0',
    colorSuccess: '#00ff88',
    colorError:   '#ff4d6d',
    borderRadius: '16px',
    shadow:       '0 8px 32px rgba(0,0,0,0.5)',
    maxWidth:     '360px',
  }"
/>
css Copy
/* Или глобально в CSS */
:root {
  --vtk-border-radius: 6px;
  --vtk-font-family:   'Inter', sans-serif;
  --vtk-max-width:     360px;
}

Все токены типизированы через ToastDesignTokens — автодополнение в IDE работает.


Headless mode

useToastState() возвращает сырые реактивные данные очереди. <ToastContainer> не нужен — рисуете UI полностью сами.

vue Copy
<script setup lang="ts">
import { useToast, useToastState } from 'vue-toast-kit'

const toast = useToast()
const { active, count } = useToastState()
</script>

<template>
  <div class="my-notifications">
    <div
      v-for="t in active"
      :key="t.id"
      :class="`notification notification--${t.options.type}`"
      @mouseenter="t.pause()"
      @mouseleave="t.resume()"
    >
      <span>{{ t.message }}</span>
      <button @click="t.dismiss()"></button>
      <div class="progress" :style="{ width: `${t.remaining.value * 100}%` }" />
    </div>
  </div>
</template>

Каждый ToastItem реактивен: remaining.value (0–1 для прогресс-бара), isPaused.value, groupCount.value. Методы: pause(), resume(), dismiss(), update(opts).


Multi-instance и изолированные контексты

createToastContext() создаёт независимую очередь. Передаётся в useToast(ctx) и <ToastContainer :context="ctx" />. Полная изоляция — разные очереди, разные настройки.

vue Copy
<script setup lang="ts">
import { createToastContext, useToast } from 'vue-toast-kit'

// Очередь для критических алертов — отдельно от обычных тостов
const alertCtx = createToastContext({ maxVisible: 3 })
const alertToast = useToast(alertCtx)

function showCritical(msg: string) {
  alertToast.error(msg, { priority: 'critical', duration: 0, closable: true })
}
</script>

<template>
  <!-- Глобальная очередь -->
  <ToastContainer position="bottom-right" />

  <!-- Критические алерты — другой угол, другой z-index -->
  <ToastContainer
    :context="alertCtx"
    position="top-center"
    :z-index="10000"
  />
</template>

Event emitter

События очереди — для интеграции с аналитикой, Sentry, логгером. Все подписчики возвращают функцию отписки.

ts Copy
import { getOrCreateGlobalContext } from 'vue-toast-kit'

const queue = getOrCreateGlobalContext().queue

queue.onAdd((item) => {
  analytics.track('toast_shown', {
    type:    item.options.type,
    message: typeof item.message === 'string' ? item.message : '[component]',
  })
})

const off = queue.onDismiss((id) => {
  analytics.track('toast_dismissed', { id })
})

// Отписаться при уничтожении компонента:
onUnmounted(off)

Rate limiting и localStorage persist

Защита от спама тостами — например при быстром клике или массовом приходе ошибок с сервера:

ts Copy
import { createToastContext } from 'vue-toast-kit'

// Не более 3 тостов в секунду, лишние молча отбрасываются
const ctx = createToastContext({ rateLimit: 3, rateLimitWindowMs: 1000 })

Или через плагин глобально:

ts Copy
app.use(VueToastPlugin, { rateLimit: 5 })

Тосты с persist: true переживают перезагрузку страницы — восстанавливаются из localStorage:

ts Copy
app.use(VueToastPlugin, { persistStorage: true })

// Этот тост вернётся после перезагрузки
toast.warning('Обслуживание сервера в 03:00', { persist: true, duration: 0 })

Nuxt 3

ts Copy
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['vue-toast-kit/nuxt'],
  vueToastKit: {
    position: 'top-right',
    theme: 'system',
    maxVisible: 5,
  },
})

Модуль автоматически добавляет плагин на клиент, настраивает авто-импорты useToast, useToastState, createToastContext, синглтона toast и компонента <ToastContainer>. CSS подключается автоматически — никакого ручного импорта.

SSR-safe: тосты, отправленные до монтирования <ToastContainer>, буферируются и сбрасываются после монтирования.


Сценарии в реальных проектах

CRM: операции с записями

В CRM удаляют записи, переносят задачи, архивируют сделки. Пользователь должен видеть результат и иметь возможность отменить. Критические ошибки должны пробиваться сквозь очередь тостов.

ts Copy
// composables/useDealActions.ts
import { toast } from 'vue-toast-kit'

export function useDealActions() {
  async function archiveDeal(deal: Deal) {
    // Помечаем как удалённую локально — UI реагирует мгновенно
    store.markArchived(deal.id)

    toast.undo(`Сделка "${deal.title}" архивирована`, {
      undo: {
        label:    'Отменить',
        duration: 7000,
        onUndo: async () => {
          store.unmarkArchived(deal.id)
          await api.deals.restore(deal.id)
          toast.success('Сделка восстановлена')
        },
      },
      onAutoClose: async () => {
        // Таймер вышел — окончательно архивируем
        await api.deals.archive(deal.id)
      },
    })
  }

  async function moveDeal(deal: Deal, stage: Stage) {
    await toast.promise(
      api.deals.move(deal.id, stage.id),
      {
        loading: `Перемещаем в "${stage.name}"…`,
        success: `Сделка перемещена в "${stage.name}"`,
        error:   (e) => `Ошибка: ${(e as Error).message}`,
      },
    )
  }

  async function massDelete(ids: string[]) {
    const result = await toast.promise(
      api.deals.massDelete(ids),
      {
        loading: `Удаляем ${ids.length} записей…`,
        success: (res) => `Удалено ${res.deleted} записей`,
        error:   'Не удалось выполнить массовое удаление',
      },
    )

    if (result.failed > 0) {
      // Критическая ошибка пробивается сквозь очередь
      toast.warning(`${result.failed} записей не удалось удалить`, {
        priority: 'high',
        duration: 8000,
      })
    }
  }

  return { archiveDeal, moveDeal, massDelete }
}

Undo-логика атомарна: либо пользователь успевает отменить — тогда onUndo, либо нет — тогда onAutoClose. Оба колбека не могут сработать одновременно.


Мониторинг: приоритетная очередь с группировкой

Дашборд мониторинга получает события с серверов в реальном времени. Критические инциденты должны выбиваться на первый план, массовые однотипные события — группироваться, чтобы не спамить экран.

ts Copy
// composables/useMonitoring.ts
import { toast, createToastContext } from 'vue-toast-kit'

// Отдельный контекст для мониторинга — не смешивается с UI-нотификациями
const monitorCtx = createToastContext({
  maxVisible: 5,
  rateLimit:  10,
  rateLimitWindowMs: 2000,
})

const alertToast = useToast(monitorCtx)

// Аналитика всех алертов
monitorCtx.queue.onAdd((item) => {
  logger.info('alert_shown', {
    type:     item.options.type,
    priority: item.options.priority,
    message:  item.message,
  })
})

function handleServerEvent(event: ServerEvent) {
  if (event.severity === 'critical') {
    // Критическое — пробивается сквозь очередь, не закрывается само
    alertToast.error(event.message, {
      priority: 'critical',
      duration: 0,
      closable: true,
      groupKey: `critical-${event.service}`,
    })
    return
  }

  if (event.severity === 'warning') {
    // Предупреждения группируются по сервису
    alertToast.warning(event.message, {
      priority: 'high',
      duration: 10_000,
      groupKey: `warn-${event.service}`,
    })
    return
  }

  // Информационные — низкий приоритет, группируются вместе
  alertToast.info(event.message, {
    priority: 'low',
    duration: 5000,
    groupKey: 'info-events',
  })
}
vue Copy
<!-- В App.vue — два отдельных контейнера -->
<template>
  <RouterView />

  <!-- Обычные UI-нотификации -->
  <ToastContainer position="bottom-right" />

  <!-- Алерты мониторинга — отдельный угол, отдельная очередь -->
  <ToastContainer
    :context="monitorCtx"
    position="top-right"
    :max-visible="5"
    :z-index="10000"
    :stack-mode="true"
  />
</template>

Критические инциденты вытесняют информационные тосты из видимой зоны за счёт приоритетов. Десятки однотипных предупреждений складываются в один тост с +N. Stack mode сжимает несколько алертов в стопку — не перекрывают интерфейс.


Загрузчик файлов: параллельные промисы

Пользователь загружает несколько файлов одновременно. Для каждого — отдельный тост с прогрессом. Успех и ошибки обрабатываются независимо.

ts Copy
// composables/useUploader.ts
import { toast } from 'vue-toast-kit'

export function useUploader() {
  async function uploadFile(file: File): Promise<UploadResult> {
    const id = toast.loading(`Загружаем ${file.name}…`)

    try {
      // Стримим прогресс через updateMessage
      const result = await uploadWithProgress(file, (percent) => {
        toast.updateMessage(id, `${file.name}${percent}%`)
      })

      toast.update(id, {
        type:     'success',
        message:  `${file.name} загружен (${formatBytes(result.size)})`,
        duration: 4000,
        closable: true,
      })

      return result
    } catch (err) {
      toast.update(id, {
        type:     'error',
        message:  `${file.name}: ${(err as Error).message}`,
        duration: 8000,
        closable: true,
        action: {
          label:   'Повторить',
          onClick: () => uploadFile(file),
        },
      })
      throw err
    }
  }

  async function uploadBatch(files: File[]) {
    // Все загрузки параллельно, каждая со своим тостом
    const results = await Promise.allSettled(files.map(uploadFile))

    const failed  = results.filter(r => r.status === 'rejected').length
    const success = results.filter(r => r.status === 'fulfilled').length

    if (failed > 0) {
      toast.warning(`${success} из ${files.length} файлов загружено. ${failed} с ошибкой.`, {
        priority: 'high',
        duration: 0,
        closable: true,
      })
    }
  }

  return { uploadFile, uploadBatch }
}

toast.updateMessage меняет текст тоста без пересоздания — прогресс обновляется на месте. Кнопка «Повторить» в тосте с ошибкой вызывает uploadFile заново. Promise.allSettled гарантирует что итоговый тост появится только после завершения всех загрузок.


Тестирование

Утилиты для unit-тестов без монтирования компонентов:

ts Copy
import { createMockToast, mockUseToast } from 'vue-toast-kit/testing'

// Мок всего API useToast — все методы как vi.fn()
describe('MyComponent', () => {
  it('вызывает toast.success после сохранения', async () => {
    const mockToast = mockUseToast()
    vi.mock('vue-toast-kit', () => ({ useToast: () => mockToast }))

    const wrapper = mount(MyComponent)
    await wrapper.find('[data-test="save"]').trigger('click')

    expect(mockToast.success).toHaveBeenCalledWith('Сохранено!')
  })
})

// Минимальный ToastItem для тестов компонентов
const item = createMockToast({
  message: 'Тестовое уведомление',
  options: { type: 'success', closable: true },
})

NPM: https://www.npmjs.com/package/vue-toast-kit
GitHub: https://github.com/macrulezru/vue-toast-kit

Читать далее

24.05.2026

vue-storage-kit — реактивное хранилище для Vue 3 с шифрованием, миграциями и синхронизацией вкладок

Пакет, который закрывает всё что нужно для работы с localStorage, sessionStorage, IndexedDB и cookies во Vue 3 — TTL, AES-GCM шифрование, схема-миграции, кросс-вкладочная синхронизация и Pinia-персист, без единой лишней зависимости.

Метки
vue3typescriptlocalstorageindexeddbpinia
23.05.2026

Command Palette для Vue 3 — fuzzy-поиск, вложенные палитры и глобальные хоткеи из коробки

@macrulez/vue-command-palette — готовый Command+K интерфейс для Vue 3 с fuzzy-поиском, группами, вложенными палитрами, историей команд и полной поддержкой тем. Единственная peer-зависимость — Vue 3.

Метки
vuevue3command-palettetypescriptopen-source