├── .gitattributes
├── .github
├── FUNDING.yml
└── workflows
│ └── publish.yml
├── .eslintrc
├── media
├── toast.gif
└── toast2.gif
├── .husky
└── pre-commit
├── postcss.config.js
├── vite.config.ts
├── renovate.json
├── .editorconfig
├── example
├── index.html
├── index.js
└── public
│ └── README.md
├── LICENSE
├── README.md
├── .gitignore
├── package.json
├── tsconfig.json
└── src
├── style.css
└── index.ts
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: 2nthony
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@2nthony"
3 | }
4 |
--------------------------------------------------------------------------------
/media/toast.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2nthony/vercel-toast/HEAD/media/toast.gif
--------------------------------------------------------------------------------
/media/toast2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2nthony/vercel-toast/HEAD/media/toast2.gif
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('postcss-preset-env')({
4 | stage: 0,
5 | }),
6 | ],
7 | }
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "packageRules": [
4 | {
5 | "matchUpdateTypes": ["minor", "patch"],
6 | "matchCurrentVersion": "!/^0/",
7 | "automerge": true
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 | [](https://npmjs.com/package/vercel-toast)
4 | [](https://npmjs.com/package/vercel-toast)
5 |
6 | Framework-agnostic vercel design's toast component
7 |
8 | 
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 | [](https://github.com/sponsors/2nthony)
60 |
61 | ## License
62 |
63 | MIT © [2nthony](https://github.com/sponsors/2nthony)
64 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------