Giter Club home page Giter Club logo

vue-sonner's Introduction

Sonner for Vue

NPM Minzip Package NPM Download

An opinionated toast component for Vue. It's a Vue port of Sonner

Preview

vue-sonner.mp4

Introduction

Vue Sonner is an opinionated toast component for Vue. It's customizable, but styled by default. Comes with a swipe to dismiss animation.

Table of Contents

TOC

Installation

To start using the library, install it in your project:

pnpm install vue-sonner
or
yarn add vue-sonner

Usage

For Vue 3

<!-- App.vue -->
<template>
  <Toaster />
  <button @click="() => toast('My first toast')">Render a toast</button>
</template>

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

For Nuxt 3

Define a nuxt plugin

// plugins/sonner.client.ts
import { Toaster, toast } from 'vue-sonner'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.component('Toaster', Toaster)

  return {
    provide: {
      toast
    }
  }
})

Use Toaster component and $toast function anywhere in the Vue SFC

<!-- app.vue -->
<template>
  <div>
    <NuxtPage />
    <Toaster position="top-right" />
    <button @click="() => $toast('My first toast')">Render a toast</button>
  </div>
</template>

<script setup lang="ts">
  // alternatively, you can also use it here
  const { $toast } = useNuxtApp()
</script>

Add the build transpile for vue-sonner

// nuxt.config.ts
import { defineNuxtConfig } from 'nuxt/config'

export default defineNuxtConfig({
  ...
  build: {
    transpile: ['vue-sonner']
  }
})

CDN Link

EMS version

https://cdn.jsdelivr.net/npm/vue-sonner/+esm

UMD version

https://www.unpkg.com/[email protected]/lib/vue-sonner.umd.cjs

Types

Default

Most basic toast. You can customize it (and any other type) by passing an options object as the second argument.

toast('Event has been created')

With custom description:

toast('Event has been created', {
  description: 'Monday, January 3rd at 6:00pm'
})

Success

Renders a checkmark icon in front of the message.

toast.success('Event has been created')

Error

Renders an error icon in front of the message.

toast.error('Event has not been created')

Action

Renders a button.

toast('Event has been created', {
  action: {
    label: 'Undo',
    onClick: () => console.log('Undo')
  }
})

Promise

Starts in a loading state and will update automatically after the promise resolves or fails.

You can pass a function to the success/error messages to incorporate the result/error of the promise.

toast.promise(() => new Promise((resolve) => setTimeout(resolve, 2000)), {
  loading: 'Loading',
  success: (data: any) => 'Success',
  error: (data: any) => 'Error'
})

Custom Component

You can pass a Vue Component as the first argument instead of a string to render custom Component while maintaining default styling. You can use the headless version below for a custom, unstyled toast.

<script lang="ts" setup>
  import { defineComponent, h, markRaw } from 'vue'

  const CustomDiv = defineComponent({
    setup() {
      return () =>
        h('div', {
          innerHTML: 'A custom toast with unstyling'
        })
    }
  })

  toast(markRaw(CustomDiv))
</script>

Customization

Headless

You can use toast.custom to render an unstyled toast with custom jsx while maintaining the functionality.

<script lang="ts" setup>
import { markRaw } from 'vue'

import HeadlessToast from './HeadlessToast.vue'

toast.custom(markRaw(HeadlessToast), { duration: 999999 })
</script>

Theme

You can change the theme using the theme prop. Default theme is light.

<Toaster theme="dark" />

Position

You can change the position through the position prop on the <Toaster /> component. Default is top-right.

<!-- Available positions -->
<!-- top-left, top-center, top-right, bottom-left, bottom-center, bottom-right -->

<Toaster position="top-center" />

Expanded

Toasts can also be expanded by default through the expand prop. You can also change the amount of visible toasts which is 3 by default.

<Toaster expand :visibleToasts="9" />

Styling for all toasts

You can style your toasts globally with the toastOptions prop in the Toaster component.

<Toaster
  :toastOptions="{
    style: { background: 'red' },
    class: 'my-toast',
    descriptionClass: 'my-toast-description'
  }"
/>

Styling for individual toast

toast('Event has been created', {
  style: {
    background: 'red'
  },
  class: 'my-toast',
  descriptionClass: 'my-toast-description'
})

Tailwind CSS

The preferred way to style the toasts with tailwind is by using the unstyled prop. That will give you an unstyled toast which you can then style with tailwind.

<Toaster
  :toastOptions="{
    unstyled: true,
    classes: {
      toast: 'bg-blue-400',
      title: 'text-red-400',
      description: 'text-red-400',
      actionButton: 'bg-zinc-400',
      cancelButton: 'bg-orange-400',
      closeButton: 'bg-lime-400'
    }
  }"
/>

You can do the same when calling toast().

toast('Hello World', {
  unstyled: true,
  classes: {
    toast: 'bg-blue-400',
    title: 'text-red-400 text-2xl',
    description: 'text-red-400',
    actionButton: 'bg-zinc-400',
    cancelButton: 'bg-orange-400',
    closeButton: 'bg-lime-400'
  }
})

Styling per toast type is also possible.

<Toaster 
  :toastOptions="{
    unstyled: true,
    classes: {
      error: 'bg-red-400',
      success: 'text-green-400',
      warning: 'text-yellow-400',
      info: 'bg-blue-400',
    }
  }"
/>

Changing Icon

You can change the default icons using slots:

<Toaster>
  <template #loading-icon>
    <LoadingIcon />
  </template>
  <template #success-icon>
    <SuccessIcon />
  </template>
  <template #error-icon>
    <ErrorIcon />
  </template>
  <template #info-icon>
    <InfoIcon />
  </template>
  <template #warning-icon>
    <WarningIcon />
  </template>
</Toaster>

Close button

Add a close button to all toasts that shows on hover by adding the closeButton prop.

<Toaster closeButton />

Rich colors

You can make error and success state more colorful by adding the richColors prop.

<Toaster richColors />

Custom offset

Offset from the edges of the screen.

<Toaster offset="80px" />

Programmatically remove toast

To remove a toast programmatically use toast.dismiss(id).

const toastId = toast('Event has been created')

toast.dismiss(toastId)

You can also use the dismiss method without the id to dismiss all toasts.

toast.dismiss()

Programmatically remove toast

You can change the duration of each toast by using the duration property, or change the duration of all toasts like this:

<Toaster :duration="10000" />
toast('Event has been created', {
  duration: 10000
})

// Persisent toast
toast('Event has been created', {
  duration: Infinity
})

On Close Callback

You can pass onDismiss and onAutoClose callbacks. onDismiss gets fired when either the close button gets clicked or the toast is swiped. onAutoClose fires when the toast disappears automatically after it's timeout (duration prop).

toast('Event has been created', {
  onDismiss: (t) => console.log(`Toast with id ${t.id} has been dismissed`),
  onAutoClose: (t) =>
    console.log(`Toast with id ${t.id} has been closed automatically`)
})

Keyboard focus

You can focus on the toast area by pressing βŒ₯/alt + T. You can override it by providing an array of event.code values for each key.

<Toaster hotkey="['KeyC']" />

Inspiration

  • sonner - An opinionated toast component for React.

License

MIT @xiaoluoboding

vue-sonner's People

Contributors

cernymatej avatar cirolosapio avatar cjboy76 avatar haythamasalama avatar itstpm avatar iyoub avatar jdm1219 avatar motea927 avatar sadeghbarati avatar saeid-za avatar thwonghin avatar wobsoriano avatar xiaoluoboding avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

vue-sonner's Issues

Global config

Is there a way to set global config for all toasts in the plugin folder for nuxt

like position & duration

Toast with type action don't work on mobile

Hello,

I'm currently working with vue-sonner for handling my toast notifications. I've encountered an issue when using a toast in the following manner:

toast.success('Card added!', {
  action: {
    label: 'Close',
    onClick: () => { console.log("yes") },
  },
})

The onClick event is not getting triggered on mobile devices. This issue persists both in a simulator with touchscreen capabilities and on an actual iPhone.

Using custom components with Nuxt

Hi,

I'm trying to figure out how to pass in a custom component when using Nuxt.

I've followed the instructions and created a plugin, but I'm not sure how/where to pass in the custom component

Thanks!

Toast hover warning

Whenever I hover over rendered toast, warning appears in the console saying Write operation failed: computed value is readonly

image

Everything is deleted after <Toaster />, and sometimes the toast() function works, sometimes not

I have my vue app setup and I followed the instructions, but even if I just copy-paste the first example, the button is not there, and even if it is there (explained in next section), on click it says Uncaught TypeError: toast is not a function . Am I possibly missing something?

When I add the to my root, nothing happens, but if I register it as a component ( app.component('Toaster', Toaster); ), the toaster shows up, but my entire content is just disappeared (even the "give me a toast" button provided)
Noticed that if I put the before anything, they disappear, but if I put it after anything, they will not disappear. It's like it deletes everything that comes after it.

Example:

<div>I will show up</div>
    <Toaster />
<div>I will NOT show up</div>

I'm using Vue 3.

I'm not sure what decides if the toast method works or not, but if I can get more info, I'll include it. My main problem/concern is the "everything disappears after " part.

Error: when pressing ESC key

From anywhere in the app if i press ESC i get following error:

Uncaught TypeError: v.contains is not a function at HTMLDocument.n (vue-sonner.js?v=5d769250:669:157)

Programatically dismiss custom toast

Looking at the code implementation, the custom toast function doesn't return an id.
Is it possible to programatically dismiss a given custom toast without an ID?

Also, if I use the toast.dismiss() function to dismiss all active toasts, I'm getting a warning:

const renderCustomToast = () => {
  toast.custom(shallowRef(MyToastComponent), { duration: Infinity })
}

const dismissToast = () => {
  toast.dismiss()
}

This triggers the following warning

Vue warn]: Maximum recursive updates exceeded. 
This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself.
Possible sources include component template, render function, updated hook or watcher source function.

Extending Not working correctly

Does anyone know how this example in this Video happens?

scrnsht_24-01.-2024at.11.16.57.mp4

For further context i just run those to functions on mounted():

toast.info('We don\'t use cookies, but we do use local storage to save your theme preference.');
toast.info("Thanks for visiting my website! πŸŽ‰");

ERROR - Dismiss not working

The dissmiss function is not working and breaking the toaster. This is the code im running. Very simple.

toast.dismiss(toastId)

and its returning

Uncaught (in promise) Maximum recursive updates exceeded in component . This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.

Screenshot 2024-03-31 at 6 21 01β€―PM

descriptionClassName not being applied

Hello,

When I pass descriptionClassName to the toastOptions object, it does not get applied to the description text. I have this in my nuxt plugin

import { Toaster } from "vue-sonner";

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.component("Toaster", {
    setup(props, ctx) {
      return () =>
        h(Toaster, {
          toastOptions: {
            className: "!bg-background !text-foreground !border-border",
            descriptionClassName: "!text-muted-foreground",
          },
          ...props,
          ...ctx.attrs,
        });
    },
  });
});

The className gets applied but when I check the browser elements, for the description, I see undefined

image

Bug: Can't create new toasts after dismiss

Enviroment

System:
OS: Linux 6.5 Ubuntu 22.04.4 LTS 22.04.4 LTS (Jammy Jellyfish)
CPU: (8) x64 Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
Memory: 6.59 GB / 15.45 GB
Container: Yes
Shell: 5.8.1 - /usr/bin/zsh
Binaries:
Node: 21.7.1 - /usr/bin/node
Yarn: 1.22.21 - /usr/bin/yarn
npm: 10.4.0 - /usr/local/bin/npm
pnpm: 8.15.4 - /usr/bin/pnpm
bun: 1.0.25 - ~/.bun/bin/bun
Browsers:
Chrome: 122.0.6261.111

Dependencies:

{
  "dependencies": {
    "vue": "^3.4.15",
    "vue-router": "^4.2.5",
    "vue-sonner": "^1.1.2"
  },
  "devDependencies": {
    "@rushstack/eslint-patch": "^1.3.3",
    "@tsconfig/node20": "^20.1.2",
    "@types/node": "^20.11.10",
    "@vitejs/plugin-vue": "^5.0.3",
    "@vue/eslint-config-prettier": "^8.0.0",
    "@vue/eslint-config-typescript": "^12.0.0",
    "@vue/tsconfig": "^0.5.1",
    "eslint": "^8.49.0",
    "eslint-plugin-vue": "^9.17.0",
    "npm-run-all2": "^6.1.1",
    "prettier": "^3.0.3",
    "typescript": "~5.3.0",
    "vite": "^5.0.11",
    "vue-tsc": "^1.8.27"
  }
}

Issue

Cannot create a new toasts after dismissing a toasts.

Expected Behavior:

Calling a new toasts should appear even after dismissing a toasts. Atleast it works this way in emilkowalski's sonner library.

Example:

<script setup lang="ts">
import { Toaster, toast } from 'vue-sonner'
const handleClick = async () => {
  // Show a toast
  const loadingToast = toast.loading('Loading...')

  // Simulate an API call
  await new Promise((resolve) => setTimeout(resolve, 4000))

  // Dismiss the toasts
  toast.dismiss(loadingToast)

  // Show a success toast
  // ERROR: NOT SHOWING
  toast.success('Success!')
}
</script>

<template>
  <button @click="handleClick">Tes</button>
  <Toaster />
</template>

Demo

vue-sonner-demo.webm

Logs

image

Add easy feature

Would be a prop called parseHTML to read descriptions or messages as html.

[types.ts] export interface ToastT { ..., parseHtml: boolean }

[Toast.vue] <template v-if="toast.description"> <template v-if="toast.parseHtml"> <div data-description="" :class="descriptionClass + toastDescriptionClass" v-html="toast.description" /> </template> <template v-else> <div data-description="" :class="descriptionClass + toastDescriptionClass"> {{ toast.description }} </div> </template> </template>

[Bug]: error on hitting `Esc` button

When a sonner component is used in page, whenever user hits escape button from keyboard, an error is logged in console:

Uncaught TypeError: te.contains is not a function

Reproduction Steps:

  1. go to sooner demo page
  2. focus the page if not focued
  3. git esc button
  4. open console tab in browser

[Vue warn]: Failed to resolve component: Toaster

Description

Getting the following warning:

[Vue warn]: Failed to resolve component: Toaster 
If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.

Currently using Nuxt3, using this package as a plugin.

vue-sonner.client.ts

export default defineNuxtPlugin((nuxtApp) => {
	nuxtApp.vueApp.component('Toaster', Toaster)

	return {
		provide: {
			toast,
		},
	}
})

And then in my app.vue its implemented like this:

<ClientOnly>
     <Toaster
          position="top-right"
	  :rich-colors="true"
     />
</ClientOnly>

Its just a warning, not a big deal but was curious if there is something on my end that I am missing, or if the package itself needs to handle this warning.

Thanks!

can we dismiss the toast programmatically? `toast.dismiss()` does not work

i am working on GUI using wails, and I tried to make my own toast because somehow the toast can't be styled using the toast option you provided using tailwind. and the toast itself is not really match up the design of my app, so i need to make my own anyway

the problem is, when i tried to use my own component, the value from the option somehow still appearing outside my component. and i am struggling to dismiss the toast because toast.dismiss() is not working. i don't know if this has something to do with wails or not

btw, i use vue-shadcn port
here is my implementation

// some imports

export type ToastOption = Omit<ExternalToast, 'invert' | 'icon' | 'important' | 'style' | 'unstyled' | 'descriptionClassName' | 'className' | 'promise' /** | 'action' */ > & {
    type?: NonNullable<Parameters<typeof toastVariants>[0]>['text']
    action?: {
        label?: string,
        icon?: Component,
        onClick: () => void
    }
}

export type ToastProps = ToastOption & {
    title: string
}

export function toast(title: string, data?: ToastOption) {    
    callToast(markRaw(
        defineComponent({ render: () => h(Toast, { title, ...data }) })), 
        { 
            ...data,
            // description: undefined,
            // onAutoClose: data?.onAutoClose,
            // onDismiss: data?.onDismiss,
            // duration: data?.duration,
            // cancel: data?.cancel,
            // id: data?.id || new Date().getTime().toString(),
        }
    )
}

image

Possible bugs when using fixed

The popup toast is covered by other components.
image

At first I thought it might be the z-index that was causing this, however after I changed it to an extremely large value the situation remained unchanged. At this point I noticed that the section tag was in a strange place in the DOM - it was not under the root component nor under the component where the toaster is located.
This is shown in the image below:
image

I realised that it was probably fixed causing the problem, so I cleared it and eventually the toast was able to display properly.
image
image

This seems to be caused by the DOM node being added to the wrong location...
I'm sorry, but I'm not really sure how this component works, so I can't give any more information.

Swipeable sonner

Hello, I see that methods to manage the swipe are in place, I expect this one to sweep the toast but it seems that it does not work. Is this a bug?

Thank you in advance for your feedback.

custom component - option to keep style

I have a scenario where i'd like to render a custom vue component, and i do so like this:

toast.message(
  markRaw(
    defineComponent({
      render() {
        return h(ChatToast, { message })
      },
    })
  )
)

the problem here is that the component always gets rendered in headless mode. an option to keep the styles would be useful, for cases where you just want to render the inner part of the notification, using a custom component

Make Promise "awaitable"

Hello,

I was using the package the other day and noticed that using the promise method does not allow me to await the results before moving to the next line in the code.

I have this function that I use with vee-validate. It will send a request to the API. When I use the toast.promise method, the line after gets executed immediately.

const onSubmit = handleSubmit(async (values) => {
  const promise = () => new Promise((resolve) => setTimeout(resolve, 3000));
  // I am usign Nuxt3 and have the `toast` auto-imported as `useSonner`
  // The `await` keyword does nothing here as the `promise` signature is like this: `promise: (promise: PromiseT, data?: PromiseData | undefined) => string | number`
  await useSonner.promise(promise, {
    loading: "Updating your settings...",
    success: (d) =>
      values.notify === "none"
        ? "You will no longer receive notifications"
        : `You will be notified by ${values.notify}`,
    error: (e) => "Error! Your information could not be sent to our servers!",
  });
  // This gets called before the promise is resolved
  console.log("Please wait on the above to finish...");
});

Can this be updated to return a promise please? Thanks.
Something like promise: (promise: PromiseT, data?: PromiseData | undefined) => Promise<string | number>

Add an option to freeze toasts on hover / window blur

I think it would be useful to have an option to pause the timeout timer of the toasts when the browser window loses focus / the mouse cursor hovers over one of the toasts.
What do you think?
I'd be happy to help implement it.

Why so many issues for such as simple plugin?

Everything below tested in Nuxt 3.

  • Example to define it as a plugin leads to hydration missmatch, even when <Toaster /> is used within <ClientOnly> component
  • Using headless option toast.custom(VSonner) does not really do anything
  • Cannot customize CSS Properties (variables) without using !important
  • Passing unstyled to toastOptions does not do anything

Also, if someone managed to make headless option work, does it receive props or something to get idea if it's warning/error/success/info, what's the title/description, etc? Otherwise, from the docs, headless options seems more like static toast, rather than headless (fully customizable)

Please let me know how to make it work properly?

Persitent accross layouts in Nuxt 3

Hey,

I'm facing an issue when displaying a toast and navigating to a new page on a different layout with Nuxt 3 + Vue Sonner; the toast is not displayed.

I have the component in both of my layouts, but the toast is not fired.

Here's how I fired the toast:

await $toast.error('You need to be signed in to do this action');
await navigateTo({ name: 'auth' });

And how one of my layout looks like

<template>
	<div class="relative h-screen">
		<div><slot /></div>
		<client-only>
			<Toaster
				position="bottom-right"
				:toastOptions="{
					className: '!rounded !shadow !text-sm !p-2.5',
				}"
			/>
		</client-only>
	</div>
</template>

Any ideas on how to fix this?

Dynamic Positioning not working

If I have a Toaster defined like:

<Toaster position="bottom-center" />

and I call

toast.success(
    `Blah`, { position: 'top-center' }
)

Nothing shows up. But if I change the .success to bottom-center it works... any guidance?

Suggestion: promise toast without success/fail

Hey,

Yesterday I was playing with the idea of rendering the promise (loading state) toast when I have some promises ongoing and loading data.
My idea was to show the loading toast when I am loading something but was stuck with the idea because success and fail toasts after promise are required.

Would it be possible (and make sense) to support promise type with having optional success and fail callbacks? So we could display loading promise but not display success/fail one. Not sure how that fits the purpose and API of the library, just putting the idea out there. :)

Allow specifying attributes on wrapper `<section>`

As far as I'm aware, it is currently not possible to style the wrapper <section>Β element, since inheritAttrs is set to false.

inheritAttrs: false

I'd like to apply isolation: isolate; to the section so that I can cover the toasts with other UI elements like slideovers etc.

What do you think would be the best solution for this? Adding a prop just for the isolation: isolate; style or allowing to provide all attributes to the wrapper element via a custom prop like wrapperAttrs ?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.