├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── publish.yml ├── .gitignore ├── .husky └── pre-commit ├── LICENSE ├── README.md ├── example ├── index.html ├── index.js └── public │ └── README.md ├── media ├── toast.gif └── toast2.gif ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── renovate.json ├── src ├── index.ts └── style.css ├── tsconfig.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@2nthony" 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2nthony 2 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Publish CI 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [lts/*] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | 29 | - name: Install pnpm 30 | run: npm i -g pnpm 31 | 32 | - name: Install deps 33 | run: pnpm i 34 | 35 | - run: pnpx semantic-release --branches main 36 | env: 37 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 38 | GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Swap 2 | [._]*.s[a-v][a-z] 3 | [._]*.sw[a-p] 4 | [._]s[a-rt-v][a-z] 5 | [._]ss[a-gi-z] 6 | [._]sw[a-p] 7 | 8 | # Session 9 | Session.vim 10 | Sessionx.vim 11 | 12 | # Temporary 13 | .netrwhist 14 | *~ 15 | 16 | # Auto-generated tag files 17 | tags 18 | 19 | # Persistent undo 20 | [._]*.un~ 21 | 22 | # Logs 23 | logs 24 | *.log 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # Runtime data 30 | pids 31 | *.pid 32 | *.seed 33 | *.pid.lock 34 | 35 | # Directory for instrumented libs generated by jscoverage/JSCover 36 | lib-cov 37 | 38 | # Coverage directory used by tools like istanbul 39 | coverage 40 | 41 | # nyc test coverage 42 | .nyc_output 43 | 44 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 45 | .grunt 46 | 47 | # Bower dependency directory (https://bower.io/) 48 | bower_components 49 | 50 | # node-waf configuration 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | build/Release 55 | 56 | # Dependency directories 57 | node_modules/ 58 | jspm_packages/ 59 | 60 | # TypeScript v1 declaration files 61 | typings/ 62 | 63 | # Optional npm cache directory 64 | .npm 65 | 66 | # Optional eslint cache 67 | .eslintcache 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variables file 79 | .env 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | 84 | # next.js build output 85 | .next 86 | 87 | # nuxt.js build output 88 | .nuxt 89 | 90 | # vuepress build output 91 | .vuepress/dist 92 | 93 | # Serverless directories 94 | .serverless 95 | 96 | # MacOS 97 | .DS_Store 98 | 99 | # Universal output 100 | dist 101 | 102 | .rpt2_cache 103 | example/public/docs 104 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2nthony (https://github.com/2nthony) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vercel-toast 2 | 3 | [![NPM version](https://badgen.net/npm/v/vercel-toast?label=&color=29BC9B)](https://npmjs.com/package/vercel-toast) 4 | [![NPM download](https://badgen.net/npm/dm/vercel-toast?label=&color=29BC9B)](https://npmjs.com/package/vercel-toast) 5 | 6 | Framework-agnostic vercel design's toast component 7 | 8 | ![](media/toast2.gif) 9 | 10 | ## Usage 11 | 12 | ### Bundler 13 | 14 | ```console 15 | npm i vercel-toast 16 | ``` 17 | 18 | ```ts 19 | // in js file 20 | import 'vercel-toast/css' 21 | import { createToast } from 'vercel-toast' 22 | 23 | createToast('Hi from vercel toast!') 24 | ``` 25 | 26 | ### Browser CDN 27 | 28 | ```html 29 | 33 | 34 | 35 | 36 | 39 | ``` 40 | 41 | ## Documentation 42 | 43 | https://vercel-toast.vercel.app 44 | 45 | ## Credits 46 | 47 | - [vercel/design's toast](https://vercel.com/design/toast) 48 | 49 | ## Contributing 50 | 51 | 1. Fork it! 52 | 2. Create your feature branch: `git checkout -b my-new-feature` 53 | 3. Commit your changes: `git commit -am 'Add some feature'` 54 | 4. Push to the branch: `git push origin my-new-feature` 55 | 5. Submit a pull request :D 56 | 57 | ## Sponsors 58 | 59 | [![sponsors](https://cdn.jsdelivr.net/gh/2nthony/sponsors-image/sponsors.svg)](https://github.com/sponsors/2nthony) 60 | 61 | ## License 62 | 63 | MIT © [2nthony](https://github.com/sponsors/2nthony) 64 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Vercel Toast 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-disable no-new */ 3 | import ghCorner from '@saika/github-corner' 4 | import '../src/style.css' 5 | import { copyCode } from 'saika-code-block-buttons' 6 | import { createToast, destroyAllToasts } from '../src' 7 | 8 | window.createToast = createToast 9 | window.destroyAllToasts = destroyAllToasts 10 | 11 | new Saika({ 12 | target: 'app', 13 | highlight: ['bash'], 14 | nav: [ 15 | { 16 | title: 'GitHub', 17 | link: 'https://github.com/2nthony/vercel-toast', 18 | }, 19 | ], 20 | router: { 21 | mode: 'history', 22 | }, 23 | 24 | plugins: [ 25 | ghCorner({ 26 | repo: '2nthony/vercel-toast', 27 | pinned: true, 28 | }), 29 | ], 30 | 31 | codeBlockButtons: [copyCode], 32 | 33 | footer: `© {{ new Date().getFullYear() }} Made with by 34 | 2nthony. 35 | Powered by Saika.`, 36 | }) 37 | -------------------------------------------------------------------------------- /example/public/README.md: -------------------------------------------------------------------------------- 1 | # vercel-toast 2 | 3 | Framework-agnostic vercel design's toast component (≈1KB Gzipped). 4 | 5 | ## Usage 6 | 7 | ### Bundler 8 | 9 | ```sh 10 | npm i vercel-toast 11 | ``` 12 | 13 | ```js 14 | // in js file 15 | import "vercel-toast/css"; 16 | import { createToast } from "vercel-toast"; 17 | 18 | createToast("Hi from vercel toast!"); 19 | ``` 20 | 21 | ### Browser CDN 22 | 23 | ```html 24 | 28 | 29 | 30 | 31 | 34 | ``` 35 | 36 | ## Explore 37 | 38 | API Docs 39 | 40 | [GitHub](https://github.com/2nthony/vercel-toast) 41 | 42 | ## Examples 43 | 44 | ### Destroy all toasts 45 | 46 | ```js 47 | import { destroyAllToasts } from "vercel-toast"; 48 | 49 | destroyAllToasts(); 50 | ``` 51 | 52 | 53 | 54 | ### Default 55 | 56 | ```js 57 | import { createToast } from "vercel-toast"; 58 | 59 | createToast("The Evil Rabbit jumped over the fence.", { 60 | timeout: 3000, // in 3 seconds 61 | }); 62 | ``` 63 | 64 | 65 | 66 | ### Multiline 67 | 68 | ```js 69 | import { createToast } from "vercel-toast"; 70 | 71 | createToast( 72 | "The Evil Rabbit jumped over the fence. The Evil Rabbit jumped over the fence. The Evil Rabbit jumped over the fence. The Evil Rabbit jumped over the fence.", 73 | { 74 | timeout: 3000, 75 | }, 76 | ); 77 | ``` 78 | 79 | 80 | 81 | ### Use a DOM node as message 82 | 83 | ```js 84 | const message = document.createElement("div"); 85 | message.innerHTML = `The Evil Rabbit jumped over the fence.`; 86 | 87 | createToast(message, { 88 | timeout: 3000, 89 | }); 90 | ``` 91 | 92 | 93 | 94 | ### Action 95 | 96 | ```js 97 | import { createToast } from "vercel-toast"; 98 | 99 | createToast("The Evil Rabbit jumped over the fence.", { 100 | action: { 101 | text: "Undo", 102 | callback(toast) { 103 | toast.destroy(); 104 | }, 105 | }, 106 | }); 107 | ``` 108 | 109 | 110 | 111 | ### Action + Cancel 112 | 113 | ```js 114 | import { createToast } from "vercel-toast"; 115 | 116 | createToast( 117 | "The Evil Rabbit jumped over the fence. The Evil Rabbit jumped over the fence again.", 118 | { 119 | action: { 120 | text: "Undo", 121 | callback(toast) { 122 | toast.destroy(); 123 | }, 124 | }, 125 | cancel: "Cancel", 126 | }, 127 | ); 128 | ``` 129 | 130 | 131 | 132 | ### With types 133 | 134 | ```js 135 | import { createToast } from "vercel-toast"; 136 | 137 | createToast("The Evil Rabbit jumped over the fence.", { 138 | timeout: 3000, 139 | type: "success", 140 | }); 141 | 142 | createToast("The Evil Rabbit jumped over the fence.", { 143 | timeout: 3000, 144 | type: "warning", 145 | }); 146 | 147 | createToast("The Evil Rabbit jumped over the fence.", { 148 | timeout: 3000, 149 | type: "error", 150 | }); 151 | 152 | createToast("The Evil Rabbit jumped over the fence.", { 153 | timeout: 3000, 154 | type: "dark", 155 | }); 156 | ``` 157 | 158 | 159 | 160 | 161 | 162 | 163 | ```js { mixin: true } 164 | { 165 | methods: { 166 | destroyAllToasts, 167 | 168 | showDefault() { 169 | createToast('The Evil Rabbit jumped over the fence.', { 170 | timeout: 3000 171 | }) 172 | }, 173 | 174 | multiline() { 175 | createToast('The Evil Rabbit jumped over the fence. The Evil Rabbit jumped over the fence. The Evil Rabbit jumped over the fence. The Evil Rabbit jumped over the fence.', { 176 | timeout: 3000 177 | }) 178 | }, 179 | 180 | domNode() { 181 | const message = document.createElement('div') 182 | message.innerHTML = 'The Evil Rabbit jumped over the fence.' 183 | createToast(message, { 184 | timeout: 3000 185 | }) 186 | }, 187 | 188 | action() { 189 | createToast('The Evil Rabbit jumped over the fence.', { 190 | action: { 191 | text: 'Undo', 192 | callback(toast) { 193 | toast.destroy() 194 | } 195 | } 196 | }) 197 | }, 198 | 199 | actionAndCancel() { 200 | createToast('The Evil Rabbit jumped over the fence. The Evil Rabbit jumped over the fence again.', { 201 | action: { 202 | text: 'Undo', 203 | callback(toast) { 204 | toast.destroy() 205 | } 206 | }, 207 | cancel: 'Cancel' 208 | }) 209 | }, 210 | 211 | withType(type) { 212 | createToast('The Evil Rabbit jumped over the fence.', { 213 | timeout: 3000, 214 | type 215 | }) 216 | } 217 | } 218 | } 219 | ``` 220 | -------------------------------------------------------------------------------- /media/toast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2nthony/vercel-toast/2b07cf06acf470d5a3678b23fabb1a31f5db0588/media/toast.gif -------------------------------------------------------------------------------- /media/toast2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2nthony/vercel-toast/2b07cf06acf470d5a3678b23fabb1a31f5db0588/media/toast2.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vercel-toast", 3 | "version": "0.0.0", 4 | "description": "Framework-agnostic vercel design's toast component", 5 | "author": "2nthony (https://github.com/2nthony)", 6 | "license": "MIT", 7 | "funding": "https://github.com/sponsors/2nthony", 8 | "repository": { 9 | "type": "git", 10 | "url": "2nthony/vercel-toast" 11 | }, 12 | "exports": { 13 | ".": { 14 | "types": "./dist/vercel-toast.d.ts", 15 | "require": "./dist/vercel-toast.js", 16 | "import": "./dist/vercel-toast.mjs" 17 | }, 18 | "./css": "./dist/vercel-toast.css", 19 | "./*": "./*" 20 | }, 21 | "main": "dist/vercel-toast.js", 22 | "module": "dist/vercel-toast.mjs", 23 | "browser": "dist/vercel-toast.global.js", 24 | "types": "dist/vercel-toast.d.ts", 25 | "files": [ 26 | "dist" 27 | ], 28 | "scripts": { 29 | "test": "echo lol", 30 | "example": "vite", 31 | "example:build": "vite build", 32 | "build": "tsup --entry.vercel-toast src/index.ts --dts --format esm,cjs,iife --minify --global-name=vercelToast", 33 | "docs": "typedoc src/index.ts --out example/public/docs --readme none", 34 | "build:docs": "npm run docs && npm run example:build", 35 | "lint": "eslint .", 36 | "lint-fix": "npm run lint -- --fix", 37 | "prepublishOnly": "npm run build" 38 | }, 39 | "devDependencies": { 40 | "@2nthony/eslint-config": "^1.0.1", 41 | "@saika/github-corner": "0.1.3", 42 | "@vitejs/plugin-vue2": "^2.2.0", 43 | "eslint": "^8.36.0", 44 | "husky": "8.0.3", 45 | "lint-staged": "13.3.0", 46 | "postcss": "8.4.49", 47 | "postcss-preset-env": "8.5.1", 48 | "saika": "2.13.10", 49 | "saika-code-block-buttons": "1.0.1", 50 | "tsup": "6.7.0", 51 | "typedoc": "0.23.27", 52 | "typescript": "5.0.4", 53 | "vite": "4.5.5", 54 | "vue": "2.7.16", 55 | "vue-template-compiler": "2.7.16", 56 | "vuedown": "3.2.0" 57 | }, 58 | "lint-staged": { 59 | "*.{ts,js,json,md}": [ 60 | "eslint --fix", 61 | "git add" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-preset-env')({ 4 | stage: 0, 5 | }), 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "packageRules": [ 4 | { 5 | "matchUpdateTypes": ["minor", "patch"], 6 | "matchCurrentVersion": "!/^0/", 7 | "automerge": true 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './style.css' 2 | 3 | const waitFor = (ms: number) => 4 | new Promise(resolve => setTimeout(resolve, ms)) 5 | 6 | const instances: Set = new Set() 7 | let container: HTMLDivElement 8 | 9 | export interface Action { 10 | text: string 11 | callback?: ActionCallback 12 | } 13 | 14 | export type Message = string | HTMLElement 15 | 16 | export type ActionCallback = (toast: Toast) => void 17 | 18 | export interface ToastOptions { 19 | /** 20 | * Automatically destroy the toast in specific timeout (ms) 21 | * @default `0` which means would not automatically destroy the toast 22 | */ 23 | timeout?: number 24 | /** 25 | * Toast type 26 | * @default `default` 27 | */ 28 | type?: 'success' | 'error' | 'warning' | 'dark' | 'default' 29 | action?: Action 30 | cancel?: string 31 | } 32 | 33 | export class Toast { 34 | message: Message 35 | options: ToastOptions 36 | el?: HTMLDivElement 37 | 38 | private timeoutId?: number 39 | 40 | constructor(message: Message, options: ToastOptions = {}) { 41 | const { timeout = 0, action, type = 'default', cancel } = options 42 | 43 | this.message = message 44 | this.options = { 45 | timeout, 46 | action, 47 | type, 48 | cancel, 49 | } 50 | 51 | this.setContainer() 52 | 53 | this.insert() 54 | instances.add(this) 55 | } 56 | 57 | insert(): void { 58 | const el = document.createElement('div') 59 | el.className = 'toast' 60 | el.setAttribute('aria-live', 'assertive') 61 | el.setAttribute('aria-atomic', 'true') 62 | el.setAttribute('aria-hidden', 'false') 63 | 64 | const { action, type, cancel } = this.options 65 | 66 | const inner = document.createElement('div') 67 | inner.className = 'toast-inner' 68 | 69 | const text = document.createElement('div') 70 | text.className = 'toast-text' 71 | inner.classList.add(type as string) 72 | 73 | if (typeof this.message === 'string') 74 | text.textContent = this.message 75 | else 76 | text.appendChild(this.message) 77 | 78 | inner.appendChild(text) 79 | 80 | if (cancel) { 81 | const button = document.createElement('button') 82 | button.className = 'toast-button cancel-button' 83 | button.textContent = cancel 84 | button.type = 'text' 85 | button.onclick = () => this.destroy() 86 | inner.appendChild(button) 87 | } 88 | 89 | if (action) { 90 | const button = document.createElement('button') 91 | button.className = 'toast-button' 92 | button.textContent = action.text 93 | button.type = 'text' 94 | button.onclick = () => { 95 | this.stopTimer() 96 | if (action.callback) 97 | action.callback(this) 98 | else 99 | this.destroy() 100 | } 101 | inner.appendChild(button) 102 | } 103 | 104 | el.appendChild(inner) 105 | 106 | this.startTimer() 107 | 108 | this.el = el 109 | 110 | container.appendChild(el) 111 | 112 | // Delay to set slide-up transition 113 | waitFor(50).then(sortToast) 114 | } 115 | 116 | destroy(): void { 117 | const { el } = this 118 | if (!el) 119 | return 120 | 121 | el.style.opacity = '0' 122 | el.style.visibility = 'hidden' 123 | el.style.transform = 'translateY(10px)' 124 | 125 | this.stopTimer() 126 | 127 | setTimeout(() => { 128 | container.removeChild(el) 129 | instances.delete(this) 130 | sortToast() 131 | }, 150) 132 | } 133 | 134 | /** 135 | * @deprecated Please use `destroy` 136 | */ 137 | destory(): void { 138 | typoWarning('destory') 139 | this.destroy() 140 | } 141 | 142 | setContainer(): void { 143 | container = document.querySelector('.toast-container') as HTMLDivElement 144 | if (!container) { 145 | container = document.createElement('div') 146 | container.className = 'toast-container' 147 | document.body.appendChild(container) 148 | } 149 | 150 | // Stop all instance timer when mouse enter 151 | container.addEventListener('mouseenter', () => { 152 | instances.forEach(instance => instance.stopTimer()) 153 | }) 154 | 155 | // Restart all instance timer when mouse leave 156 | container.addEventListener('mouseleave', () => { 157 | instances.forEach(instance => instance.startTimer()) 158 | }) 159 | } 160 | 161 | startTimer(): void { 162 | if (this.options.timeout && !this.timeoutId) { 163 | this.timeoutId = self.setTimeout( 164 | () => this.destroy(), 165 | this.options.timeout, 166 | ) 167 | } 168 | } 169 | 170 | stopTimer(): void { 171 | if (this.timeoutId) { 172 | clearTimeout(this.timeoutId) 173 | this.timeoutId = undefined 174 | } 175 | } 176 | } 177 | 178 | export function createToast(message: Message, options?: ToastOptions): Toast { 179 | return new Toast(message, options) 180 | } 181 | 182 | export function destroyAllToasts(): void { 183 | if (!container) 184 | return 185 | 186 | instances.forEach((instance) => { 187 | instance.destroy() 188 | }) 189 | } 190 | /** 191 | * @deprecated Please use `destroyAllToasts` 192 | */ 193 | export function destoryAllToasts(): void { 194 | typoWarning('destoryAllToasts') 195 | destroyAllToasts() 196 | } 197 | 198 | function sortToast(): void { 199 | const toasts = Array.from(instances).reverse().slice(0, 4) 200 | 201 | const heights: Array = [] 202 | 203 | toasts.forEach((toast, index) => { 204 | const sortIndex = index + 1 205 | const el = toast.el as HTMLDivElement 206 | const height = +(el.getAttribute('data-height') || 0) || el.clientHeight 207 | 208 | heights.push(height) 209 | 210 | el.className = `toast toast-${sortIndex}` 211 | el.dataset.height = `${height}` 212 | el.style.setProperty('--index', `${sortIndex}`) 213 | el.style.setProperty('--height', `${height}px`) 214 | el.style.setProperty('--front-height', `${heights[0]}px`) 215 | 216 | if (sortIndex > 1) { 217 | const hoverOffsetY = heights 218 | .slice(0, sortIndex - 1) 219 | .reduce((res, next) => (res += next), 0) 220 | el.style.setProperty('--hover-offset-y', `-${hoverOffsetY}px`) 221 | } 222 | else { 223 | el.style.removeProperty('--hover-offset-y') 224 | } 225 | }) 226 | } 227 | 228 | function typoWarning(method: string) { 229 | console.warn( 230 | '[vercel-toast]:', 231 | `\`${method}\` is a typo function, please use \`${method.replace( 232 | 'or', 233 | 'ro', 234 | )}\``, 235 | ) 236 | } 237 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | .toast-container { 2 | --radius: 5px; 3 | --stack-gap: 20px; 4 | --safe-area-gap: env(safe-area-inset-bottom); 5 | 6 | position: fixed; 7 | display: block; 8 | max-width: 468px; 9 | bottom: calc(var(--safe-area-gap, 0px) + 20px); 10 | right: 20px; 11 | z-index: 5000; 12 | transition: all 0.4s ease; 13 | 14 | & .toast { 15 | position: absolute; 16 | bottom: 0; 17 | right: 0; 18 | width: 468px; 19 | transition: all 0.4s ease; 20 | transform: translate3d(0, 86px, 0); 21 | opacity: 0; 22 | 23 | & .toast-inner { 24 | --toast-bg: none; 25 | --toast-fg: #fff; 26 | --toast-border-color: #eaeaea; 27 | box-sizing: border-box; 28 | border-radius: var(--radius); 29 | border: 1px solid var(--toast-border-color); 30 | display: flex; 31 | align-items: center; 32 | justify-content: space-between; 33 | padding: 24px; 34 | color: var(--toast-fg); 35 | background-color: var(--toast-bg); 36 | height: var(--height); 37 | transition: all 0.25s ease; 38 | 39 | &.default { 40 | --toast-fg: #000; 41 | --toast-bg: #fff; 42 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12); 43 | } 44 | 45 | &.success { 46 | --toast-bg: #0076ff; 47 | --toast-border-color: var(--toast-bg); 48 | } 49 | 50 | &.error { 51 | --toast-bg: #e00; 52 | --toast-border-color: var(--toast-bg); 53 | } 54 | 55 | &.warning { 56 | --toast-bg: #f5a623; 57 | --toast-border-color: var(--toast-bg); 58 | } 59 | 60 | &.dark { 61 | --toast-bg: #000; 62 | --toast-fg: #fff; 63 | --toast-border-color: #333; 64 | 65 | & .toast-button { 66 | --button-fg: #000; 67 | --button-bg: #fff; 68 | --button-border: #fff; 69 | --button-border-hover: #fff; 70 | --button-fg-hover: #fff; 71 | 72 | &.cancel-button { 73 | --cancel-button-bg: #000; 74 | --cancel-button-fg: #888; 75 | --cancel-button-border: #333; 76 | 77 | &:hover { 78 | color: #fff; 79 | border-color: var(--button-border); 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | & .toast-text { 87 | width: 100%; 88 | height: 100%; 89 | font-size: 14px; 90 | margin-top: -1px; 91 | margin-right: 24px; 92 | transition: all 0.3s ease-in; 93 | } 94 | 95 | & .toast-button { 96 | --button-fg: #000; 97 | --button-bg: #fff; 98 | --button-border: #fff; 99 | --button-border-hover: #fff; 100 | --button-fg-hover: #fff; 101 | min-width: auto; 102 | height: 24px; 103 | line-height: 22px; 104 | padding: 0 10px; 105 | font-size: 14px; 106 | background-color: var(--button-bg); 107 | color: var(--button-fg); 108 | white-space: nowrap; 109 | user-select: none; 110 | cursor: pointer; 111 | vertical-align: middle; 112 | border-radius: var(--radius); 113 | outline: none; 114 | border: 1px solid var(--button-border); 115 | transition: all 0.2s ease; 116 | 117 | &:hover { 118 | border-color: var(--button-border-hover); 119 | background-color: transparent; 120 | color: var(--button-fg-hover); 121 | } 122 | 123 | &.cancel-button { 124 | --cancel-button-bg: #fff; 125 | --cancel-button-fg: #666; 126 | --cancel-button-border: #eaeaea; 127 | margin-right: 10px; 128 | color: var(--cancel-button-fg); 129 | border-color: var(--cancel-button-border); 130 | background-color: var(--cancel-button-bg); 131 | 132 | &:hover { 133 | --cancel-button-fg: #000; 134 | --cancel-button-border: #000; 135 | } 136 | } 137 | } 138 | 139 | & .default .toast-button { 140 | --button-fg: #fff; 141 | --button-bg: #000; 142 | --button-border: #000; 143 | --button-border-hover: #000; 144 | --button-fg-hover: #000; 145 | } 146 | 147 | &:after { 148 | content: ""; 149 | position: absolute; 150 | left: 0; 151 | right: 0; 152 | top: calc(100% + 1px); 153 | width: 100%; 154 | /* This for destroy the middle toast, still keep `spread` */ 155 | height: 1000px; 156 | background: transparent; 157 | } 158 | 159 | &.toast-1 { 160 | transform: translate3d(0, 0, 0); 161 | opacity: 1; 162 | } 163 | 164 | &:not(:last-child) { 165 | --i: calc(var(--index) - 1); 166 | transform: translate3d(0, calc(1px - (var(--stack-gap) * var(--i))), 0) 167 | scale(calc(1 - 0.05 * var(--i))); 168 | opacity: 1; 169 | 170 | & .toast-inner { 171 | height: var(--front-height); 172 | 173 | & .toast-text { 174 | opacity: 0; 175 | } 176 | } 177 | } 178 | 179 | &.toast-4 { 180 | opacity: 0; 181 | } 182 | } 183 | } 184 | 185 | /* if more than 1, then apply hover effect */ 186 | .toast-container:has(.toast-2):hover { 187 | bottom: calc(var(--safe-area-gap, 0px) + 30px); 188 | } 189 | 190 | .toast-container:hover .toast { 191 | transform: translate3d( 192 | 0, 193 | calc(var(--hover-offset-y) - var(--stack-gap) * (var(--index) - 1)), 194 | 0 195 | ); 196 | 197 | & .toast-inner { 198 | height: var(--height); 199 | } 200 | 201 | & .toast-text { 202 | opacity: 1 !important; 203 | } 204 | } 205 | 206 | @media (max-width: 440px) { 207 | .toast-container { 208 | max-width: 90vw; 209 | right: 5vw; 210 | 211 | & .toast { 212 | width: 90vw; 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | "lib": [ 7 | "dom", 8 | "esnext" 9 | ] /* Specify library files to be included in the compilation. */, 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true /* Generates corresponding '.d.ts' file. */, 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "incremental": true, /* Enable incremental compilation */ 21 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 22 | // "removeComments": true, /* Do not emit comments to output. */ 23 | // "noEmit": true, /* Do not emit outputs. */ 24 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 25 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 26 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 30 | "strictNullChecks": true /* Enable strict null checks. */, 31 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 32 | "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, 33 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 34 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 35 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | /* Source Map Options */ 52 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 53 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 54 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 55 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | "skipLibCheck": true 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue2' 3 | 4 | export default defineConfig({ 5 | root: 'example', 6 | plugins: [vue()], 7 | }) 8 | --------------------------------------------------------------------------------