cincin-vue

🥂 Vue bindings for cincin: headless composables, primitives, ready-to-use skin
1,694 1
Install
npm install cincin-vue
Latest Version:0.3.1
Vue:3
License:MIT
Last Updated:Oct 19, 2026
Links: GitHub  ·  npm
Maintainer: nbarinov
cincin-vue 🥂

Vue bindings for the cincin toast library: a ready-to-use <Toaster /> for a quick start, and headless building blocks under cincin-vue/core.

Install

npm install cincin-vue

Requires Vue 3.5. The package ships untranspiled modern JS as ESM only. With yarn, add cincin next to it: the adapter declares the core as a peer, and yarn does not install peers on its own.

Quick start

<script setup lang="ts">
import { Toaster, toast } from 'cincin-vue';
</script>

<template>
  <button @click="toast.success({ title: 'Saved' })">Save</button>
  <Toaster />
</template>

<Toaster /> renders a swipeable stack that collapses to a clean edge, expands on hover or tap, and pauses its timers while open and while the tab is hidden; the stylesheet comes along with the import. toast is a package-wide store callable from anywhere on the client (calls on the server do nothing useful).

toast.error({
  title: 'Something broke',
  description: 'The request did not survive the round trip.',
  actions: [{ label: 'Retry', onClick: retry }],
});

toast.promise(upload(), {
  loading: { title: 'Uploading…' },
  success: (ms) => ({ title: `Uploaded in ${ms}ms` }),
  error: () => ({ title: 'Upload failed' }),
});

// Closing from app code: the entry goes at once, the exit still plays.
const id = toast.info({ title: 'Connected' });
toast.remove(id);

Actions

A toast takes one or two actions, and a click on either dismisses it. The handler receives the click event and can cancel that with event.preventDefault(), say to morph the toast in place by re-creating its id (the check is synchronous, so prevent before any await):

const id = toast.message({
  title: 'Message archived',
  actions: [
    {
      label: 'Undo',
      onClick: (event) => {
        event.preventDefault();
        toast.success({ title: 'Archive restored' }, { id });
      },
    },
  ],
});

Buttons render in the order you list them, left to right, and that is also their tab order: the skin never reorders a pair. Which one looks loud is variant, not position, so the two are yours to combine. primary is the outlined default and secondary drops the border to step back.

toast.message(
  {
    title: 'Invitation',
    description: 'Anna asked to join the workspace.',
    actions: [
      { label: 'Decline', variant: 'secondary', onClick: decline },
      { label: 'Accept', onClick: accept },
    ],
  },
  { duration: Infinity, dismissible: false }
);

There is no disabled on an action, on purpose. A pending Accept is better said out loud: prevent the dismiss and re-create the same id as toast.loading({ title: 'Accepting…' }), and the buttons stop existing instead of greying out.

Duration and dismissal

An update inherits the clock and the closability: duration and dismissible re-derive from the type's defaults only when the type changes. Answer a sticky ask in a different type. A confirmation morphed in the asking type inherits the open-ended clock and strands on screen with no cross, no swipe, and no expiry.

The cross is chrome, not permission. closeButton: false hides it and leaves the toast swipeable, which is what an undo toast wants: the button reads as the way out, and the gesture is still there for anyone who would rather flick it away. dismissible: false is the other thing entirely, it takes the right to close away (no cross, no swipe), and no closeButton brings the cross back.

toast.message({
  title: 'Message archived',
  closeButton: false,
  actions: [{ label: 'Undo', onClick: undoArchive }],
});

Keyboard and screen readers

Errors and warnings announce as alert, the rest as status. Each card is a tab stop of its own, named by its title and described by its description, so tabbing into the stack reads the toast before its controls; the cross and the actions follow in the order they render. The front toast comes first: Tab enters the newest and walks back through the older ones. Landing on a card opens the stack the way hover does, and the collapsed backs stay inert until then.

Alt+T moves focus onto the front toast from anywhere on the page (the hotkey prop changes or drops it), Escape hands it back to where it came from, and closing a toast from the keyboard passes the focus on to the next one. The region is a landmark named by labels.region.

Position

<Toaster position="top-center" />

Six spots: the four corners and the two edge centers. An explicit position is physical and final. The default is the bottom inline-end corner (bottom-right, bottom-left under RTL) and it is live: the Toaster watches the root's dir and follows a flip without a re-render. A server-rendered RTL page settles the corner right after hydration; pass an explicit position to skip that flip. One Toaster owns one spot: for a second corner, mount a second Toaster over its own store.

Swiping follows the corner. The default swipe-directions set is the position's outward edges (['right', 'down'] at bottom-right), so the card leaves toward the nearest viewport edge, and a set spanning both axes claims touch-action: none on the cards, trading scrolling over them away, deliberately. A center offers only its vertical edge, and the horizontal axis stays with the browser for scrolling. An explicit swipe-directions overrides the pairing.

Offset

<Toaster :offset="72" />

Somewhere in the corner already lives a chat bubble, a cart bar, a player. offset says how far the list steps inward from the edges it hangs on, so the toasts stand clear of it. The value is a distance, not a coordinate: the direction comes from the position's anchors, so one number lifts a bottom-* list up and drops a top-* list down. A number is px, a string is any CSS length, and { x, y } names both axes.

The horizontal half needs a column with a width of its own. A centered list has no near side, and on a narrow screen the list spans both edges, so there is nothing to cede sideways and only the vertical half is spent. Name both axes and one offset covers both widths: the toasts stand beside the widget on a wide screen and above it on a phone, without recomputing anything.

<Toaster :offset="{ x: clearance.x, y: clearance.y }" />

For a column that should be shorter on a phone, reach for the side inset instead. Insets own the list's bounds; the offset owns its step inward.

The distance adds to the skin's own inset, which is where env(safe-area-inset-*) lives. That split is the point: a clearance measured in JavaScript cannot carry the device's margins, because env() is not readable from there. Let the stylesheet keep the device and hand the prop only what your page knows:

const clearance = ref(0);
</script>

<template>
  <Toaster :offset="clearance" />
</template>

The prop is sugar over two custom properties, --cincin-offset-x and --cincin-offset-y, and it writes them inline. Leave it out and the channel stays with your stylesheet, media queries included; pass it and the inline value outranks them. The list slides on translate, so a value that changes animates without reflowing the page.

Toaster props

Prop What it does
toaster Your own store instead of the singleton. Read once, remount to switch.
max How many toasts are active at once, the rest queue. Live.
visible How many peek out of the collapsed stack.
position The list's corner or edge center. Defaults to the bottom inline-end corner, live against the document's dir.
offset How far the list steps inward from its edges, for a corner your page already uses. A bare value is the vertical axis; { x, y } names both.
swipe-directions Which way a card can be flicked. Defaults to the position's outward edges.
exit-duration The exit animation's length in ms. One value drives the presenter's exit clock and, published as --cincin-exit-duration, the skin's motion durations.
labels The skin's a11y vocabulary, { region, close }. Defaults 'Notifications' and 'Dismiss'.
hotkey The shortcut that moves focus onto the front toast, 'Alt+T' by default; false drops it.

Advanced

Headless

The skin is one opinion built from public primitives. When it stops fitting, drop down a level instead of fighting it.

<script setup lang="ts">
import { usePresenter, useToasts } from 'cincin-vue/core';
import Card from './Card.vue';

const { toaster } = defineProps(['toaster']);

// The exit clock finishes leaving toasts on time: declare your exit
// animation's length, no transitionend listeners needed.
const presenter = usePresenter(toaster, { max: 5, exitDuration: 400 });
const toasts = useToasts(presenter);
</script>

<template>
  <ol>
    <Card
      v-for="toast of toasts"
      :key="toast.key"
      :toast="toast"
      :presenter="presenter"
    />
  </ol>
</template>
<!-- Card.vue -->
<script setup lang="ts">
import { useToastSwipe } from 'cincin-vue/core';

const props = defineProps(['toast', 'presenter']);

const { handlers, style } = useToastSwipe({
  key: props.toast.key,
  presenter: props.presenter,
  enabled: () => props.toast.entry.dismissible,
});
</script>

<template>
  <li :data-phase="toast.phase" :style="style" v-on="handlers">
    {{ String(toast.entry.content) }}
  </li>
</template>

The composables return refs and take options as plain values, refs or getters (MaybeRefOrGetter): a getter over your props keeps an option live. The rest of the toolbox: usePresenterHolder(presenter) is the one holder of the presenter's clocks, which the sources below hold by name; useViewport({ presenter, holder, ...options }) owns the stack's attention (open under the pointer or focus, fold a delay after both leave, hold the clocks while open) and returns expanded plus the handlers to bind on the list with v-on; useStack(toasts, options) owns a cincin/dom stack layout and mirrors the rendered list into it; useSlot(element, { layout, key }) registers the card's element for measurement and reads its live slot (geometry, front/leaving for the inert rule); useVisibilityPause(holder) holds the clocks while the document is hidden; useToastEntries(toaster) subscribes to the store records instead of the showings. The primitives take their instances explicitly and carry no CSS.

Your own store

The toast singleton is a convenience, not the only way in. Build a store yourself and hand it to the Toaster:

<script setup lang="ts">
import { createToaster } from 'cincin';
import { Toaster } from 'cincin-vue';

const toaster = createToaster({ duration: 4000 });
</script>

<template>
  <Toaster :toaster="toaster" />
</template>

The prop is read once, so remount the Toaster to switch stores. For a typed content payload, createToasterContext<MyContent>() returns provideToaster with injection-aware useToaster and useToastEntries.

Browser support

Chrome 116+, Safari 17.4+, Firefox 124+, Node 20.3+ for SSR. The bundled skin's CSS wants 2024-class browsers. See cincin for the details.

Source and issues: github.com/nbarinov/cincin

Related Packages

@incuca/vue3-toaster

Lightweight toast-notification plugin for Vue 3

5,317 5
@rozie-ui/toast-vue

Idiomatic Vue headless, accessible toast / notification host (queue, auto-dismis...

1,675 1
vuetify-toast-snackbar

Basic Vue toast service that uses Vuetify Snackbar component.

47,317 127
vuetify-toast-snackbar-ng

Basic Vue toast service that uses Vuetify Snackbar component for Vuetify 2.3+

25,369 22
vue-toastflow

Framework-agnostic toast engine with a Vue 3 renderer. Typed core, smooth stack...

17,273 207

Version History

Version Released Vue License
0.3.1 >=3.5 MIT
0.3.0 >=3.5 MIT
0.2.0 >=3.5 MIT
0.1.1 >=3.5 MIT
0.1.0 >=3.5 MIT
Pre-releases (4)
Version Released Vue License
0.1.0-beta.6 >=3.5 MIT
0.1.0-beta.5 >=3.5 MIT
0.1.0-beta.4 >=3.5 MIT
0.1.0-beta.3 >=3.5 MIT