Repository: timolins/react-hot-toast
Branch: main
Commit: f339d7105c90
Files: 55
Total size: 118.4 KB
Directory structure:
gitextract_zguhd_ms/
├── .github/
│ └── workflows/
│ ├── main.yml
│ └── size.yml
├── .gitignore
├── LICENSE
├── README.md
├── jest.config.js
├── package.json
├── site/
│ ├── .gitignore
│ ├── README.md
│ ├── components/
│ │ ├── code.tsx
│ │ ├── docs-layout.tsx
│ │ ├── emoji-button.tsx
│ │ └── sections/
│ │ ├── footer.tsx
│ │ ├── splitbee-counter.tsx
│ │ ├── toast-example.tsx
│ │ └── toaster-example.tsx
│ ├── next-env.d.ts
│ ├── next.config.mjs
│ ├── package.json
│ ├── pages/
│ │ ├── _app.tsx
│ │ ├── docs/
│ │ │ ├── index.mdx
│ │ │ ├── multi-toaster.mdx
│ │ │ ├── styling.mdx
│ │ │ ├── toast-bar.mdx
│ │ │ ├── toast.mdx
│ │ │ ├── toaster.mdx
│ │ │ ├── use-toaster-store.mdx
│ │ │ ├── use-toaster.mdx
│ │ │ └── version-2.mdx
│ │ └── index.tsx
│ ├── postcss.config.js
│ ├── styles/
│ │ ├── main.css
│ │ ├── prism-theme.css
│ │ └── tailwind-utils.css
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ └── types/
│ ├── mdx.d.ts
│ └── svg.d.ts
├── src/
│ ├── components/
│ │ ├── checkmark.tsx
│ │ ├── error.tsx
│ │ ├── loader.tsx
│ │ ├── toast-bar.tsx
│ │ ├── toast-icon.tsx
│ │ └── toaster.tsx
│ ├── core/
│ │ ├── store.ts
│ │ ├── toast.ts
│ │ ├── types.ts
│ │ ├── use-toaster.ts
│ │ └── utils.ts
│ ├── headless/
│ │ └── index.ts
│ └── index.ts
├── test/
│ ├── setup.ts
│ └── toast.test.tsx
├── tsconfig.json
└── tsup.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/main.yml
================================================
name: CI
on: [push]
jobs:
build:
name: Build & test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build package
run: pnpm build
- name: Test
run: pnpm run test --ci --coverage
================================================
FILE: .github/workflows/size.yml
================================================
name: size
on: [pull_request]
jobs:
size:
runs-on: ubuntu-latest
env:
CI_JOB_NUMBER: 1
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
- run: pnpm install
- uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
*.log
.DS_Store
node_modules
.cache
coverage
dist
/headless
.vscode
.vercel
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Timo Lins
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<a href="https://react-hot-toast.com/"><img alt="react-hot-toast - Try it out" src="https://github.com/timolins/react-hot-toast/raw/main/assets/header.svg"/></a>
<div align="center">
<img src="https://badgen.net/npm/v/react-hot-toast" alt="NPM Version" />
<img src="https://badgen.net/bundlephobia/minzip/react-hot-toast" alt="minzipped size"/>
<img src="https://github.com/timolins/react-hot-toast/workflows/CI/badge.svg" alt="Build Status" />
</a>
</div>
<br />
<div align="center"><strong>Smoking hot Notifications for React.</strong></div>
<div align="center"> Lightweight, customizable and beautiful by default.</div>
<br />
<div align="center">
<a href="https://react-hot-toast.com/">Website</a>
<span> · </span>
<a href="https://react-hot-toast.com/docs">Documentation</a>
<span> · </span>
<a href="https://twitter.com/timolins">Twitter</a>
</div>
<br />
<div align="center">
<sub>Cooked by <a href="https://twitter.com/timolins">Timo Lins</a> 👨🍳</sub>
</div>
<br />
## Features
- 🔥 **Hot by default**
- 🔩 **Easily Customizable**
- ⏳ **Promise API** - _Automatic loader from a promise_
- 🕊 **Lightweight** - _less than 5kb including styles_
- ✅ **Accessible**
- 🤯 **Headless Hooks** - _Create your own with [`useToaster()`](https://react-hot-toast.com/docs/use-toaster)_
## Installation
#### With pnpm
```sh
pnpm add react-hot-toast
```
#### With NPM
```sh
npm install react-hot-toast
```
## Getting Started
Add the Toaster to your app first. It will take care of rendering all notifications emitted. Now you can trigger `toast()` from anywhere!
```jsx
import toast, { Toaster } from 'react-hot-toast';
const notify = () => toast('Here is your toast.');
const App = () => {
return (
<div>
<button onClick={notify}>Make me a toast</button>
<Toaster />
</div>
);
};
```
## Documentation
Find the full API reference on [official documentation](https://react-hot-toast.com/docs).
================================================
FILE: jest.config.js
================================================
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
};
================================================
FILE: package.json
================================================
{
"name": "react-hot-toast",
"description": "Smoking hot React Notifications. Lightweight, customizable and beautiful by default.",
"version": "2.6.0",
"author": "Timo Lins",
"license": "MIT",
"repository": "timolins/react-hot-toast",
"keywords": [
"react",
"notifications",
"toast",
"snackbar"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./headless": {
"types": "./headless/index.d.ts",
"import": "./headless/index.mjs",
"require": "./headless/index.js"
}
},
"files": [
"headless",
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"scripts": {
"start": "tsup --watch",
"build": "tsup",
"test": "jest --runInBand",
"setup": "pnpm i && cd site && pnpm i && cd .. && pnpm run link",
"link": "pnpm link ./site/node_modules/react && pnpm link ./site/node_modules/react-dom",
"size": "size-limit"
},
"husky": {
"hooks": {
"pre-commit": "prettier src --ignore-unknown --write"
}
},
"prettier": {
"printWidth": 80,
"semi": true,
"singleQuote": true,
"arrowParens": "always",
"trailingComma": "es5"
},
"size-limit": [
{
"path": "dist/index.js",
"limit": "5.5 KB"
},
{
"path": "dist/index.mjs",
"limit": "5.5 KB"
},
{
"path": "headless/index.js",
"limit": "2.5 KB"
},
{
"path": "headless/index.mjs",
"limit": "2.5 KB"
}
],
"devDependencies": {
"@jest/types": "^29.6.3",
"@size-limit/preset-small-lib": "^7.0.8",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@types/jest": "^29.5.14",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"esbuild-minify-templates": "^0.13.1",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"prettier": "^2.8.8",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"size-limit": "^7.0.8",
"ts-jest": "^29.2.5",
"tslib": "^2.8.1",
"tsup": "^6.7.0",
"typescript": "^5.7.2"
},
"dependencies": {
"csstype": "^3.1.3",
"goober": "^2.1.16"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
},
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
}
================================================
FILE: site/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
================================================
FILE: site/README.md
================================================
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
================================================
FILE: site/components/code.tsx
================================================
import clsx from 'clsx';
import Highlight, {
defaultProps,
Language,
PrismTheme,
} from 'prism-react-renderer';
const theme: PrismTheme = {
plain: {
backgroundColor: '#351e11',
color: '#d6ceff',
},
styles: [
{
types: ['comment', 'prolog', 'doctype', 'cdata', 'punctuation'],
style: {
color: '#6c6783',
},
},
{
types: ['namespace'],
style: {
opacity: 0.7,
},
},
{
types: ['tag', 'operator', 'number', 'module'],
style: {
color: '#e09142',
},
},
{
types: ['property', 'function'],
style: {
color: '#9a86fd',
},
},
{
types: ['tag-id', 'selector', 'atrule-id'],
style: {
color: '#eeebff',
},
},
{
types: ['attr-name'],
style: {
color: '#c4b9fe',
},
},
{
types: [
'boolean',
'string',
'entity',
'url',
'attr-value',
'keyword',
'control',
'directive',
'unit',
'statement',
'regex',
'at-rule',
'placeholder',
'variable',
],
style: {
color: '#ffcc99',
},
},
{
types: ['deleted'],
style: {
textDecorationLine: 'line-through',
},
},
{
types: ['inserted'],
style: {
textDecorationLine: 'underline',
},
},
{
types: ['italic'],
style: {
fontStyle: 'italic',
},
},
{
types: ['important', 'bold'],
style: {
fontWeight: 'bold',
},
},
{
types: ['important'],
style: {
color: '#c4b9fe',
},
},
],
};
export const Code: React.FC<{
snippet: string;
language?: Language;
className?: string;
}> = (props) => {
const language = props.language || 'jsx';
return (
<Highlight
{...defaultProps}
code={props.snippet}
theme={theme}
language={language}
>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre
className={clsx(
props.className,
className,
'h-full w-full rounded-lg p-4 overflow-x-auto flex flex-col items justify-center'
)}
style={style}
>
{tokens.map((line, i) => {
if (tokens.length - 1 === i && line[0].empty) {
return null;
}
return (
<div {...getLineProps({ line, key: i })} key={i}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} key={key} />
))}
</div>
);
})}
</pre>
)}
</Highlight>
);
};
================================================
FILE: site/components/docs-layout.tsx
================================================
import * as React from 'react';
import { Toaster } from 'react-hot-toast';
import { NextSeo } from 'next-seo';
import Link from 'next/link';
import { Footer } from './sections/footer';
import Logo from '../assets/logo-small.svg';
const TableItem: React.FC<{
href: string;
children?: React.ReactNode;
}> = ({ children, href }) => (
<Link href={href}>
<a className="rounded px-3 py-1.5 transition-colors duration-200 relative block hover:text-toast-500 text-toast-700">
{children}
</a>
</Link>
);
const TableHeader: React.FC<{
children?: React.ReactNode;
}> = ({ children }) => (
<span className="px-3 mt-3 mb-1 text-sm font-semibold tracking-wide text-toast-900 uppercase">
{children}
</span>
);
export default function DocsLayout({ meta, children }) {
return (
<div className="bg-toast-50 bg-opacity-50 min-h-screen flex flex-col">
<NextSeo
titleTemplate="%s - react-hot-toast"
title={meta.title}
openGraph={{
images: [
{
url: `https://react-hot-toast.com/social-image.png`,
width: 1200,
height: 630,
},
],
}}
/>
<div className="flex-1 mx-auto px-2 max-w-4xl w-full">
<header className=" col-start-1 col-end-6 mt-12 mb-16 px-2 flex justify-between items-center">
<Link href="/">
<Logo
className="cursor-pointer"
aria-label="react-hot-toast Logo"
/>
</Link>
<a
className="flex text-toast-600 underline"
href="https://github.com/timolins/react-hot-toast"
>
GitHub
</a>
</header>
<div className="md:flex md:space-x-4">
<nav className="font-medium rounded-lg ">
<div className="flex flex-col mb-8 sticky top-0">
<TableHeader>Overview</TableHeader>
<TableItem href="/docs">Get Started</TableItem>
<TableHeader>API</TableHeader>
<TableItem href="/docs/toast">toast()</TableItem>
<TableItem href="/docs/toaster">{`Toaster`}</TableItem>
<TableItem href="/docs/toast-bar">{`ToastBar`}</TableItem>
<TableItem href="/docs/use-toaster">useToaster()</TableItem>
<TableItem href="/docs/use-toaster-store">
useToasterStore()
</TableItem>
<TableHeader>Guides</TableHeader>
<TableItem href="/docs/styling">Styling</TableItem>
<TableItem href="/docs/multi-toaster">Multi Toaster</TableItem>
<TableHeader>Releases</TableHeader>
<TableItem href="/docs/version-2">New in 2.0</TableItem>
</div>
</nav>
<main className="col-span-4 w-full prose prose-toast text-toast-900 flex-1">
{children}
</main>
</div>
</div>
<Footer />
<Toaster />
</div>
);
}
================================================
FILE: site/components/emoji-button.tsx
================================================
export const EmojiButton: React.FC<{
onClick: () => void;
emoji: string | React.ReactElement;
children?: React.ReactNode;
}> = ({ onClick, children, emoji }) => (
<button
className="rounded bg-white text-sm font-semibold py-2 px-2 shadow-small-button flex items-center"
onClick={onClick}
>
<span
style={{
fontFamily:
'"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji',
}}
>
{emoji}
</span>
<div className="flex-1 px-3">{children}</div>
</button>
);
================================================
FILE: site/components/sections/footer.tsx
================================================
import React from 'react';
import Link from 'next/link';
export function Footer({ noBadge }: { noBadge?: boolean }) {
return (
<footer className="container relative justify-center my-8 flex flex-col items-center space-y-4">
<div className="flex space-x-4">
<a
className="underline"
href="https://github.com/timolins/react-hot-toast"
>
GitHub
</a>
<Link href="/docs">
<a className="underline">Docs</a>
</Link>
<a className="underline" href="https://twitter.com/timolins">
Twitter
</a>
</div>
<div className="text-toast-600">
<span>© {new Date().getFullYear()} react-hot-toast</span>
{' · '}
<span>
<span>Built by </span>
<a className="underline" href="https://timo.sh">
Timo Lins
</a>
</span>
</div>
{!noBadge && (
<div>
<a
href="https://splitbee.io/?ref=rht"
data-splitbee-event="Click Splitbee Analytics"
data-splitbee-event-location="Footer"
>
<img
src="https://splitbee-cdn.fra1.cdn.digitaloceanspaces.com/static/badge/splitbee-badge.svg"
alt="Analytics by Splitbee.io"
/>
</a>
</div>
)}
</footer>
);
}
================================================
FILE: site/components/sections/splitbee-counter.tsx
================================================
import React from 'react';
import clsx from 'clsx';
export const useSplitbeeCount = <T extends string>(
event: T,
token: string
): number => {
const [data, setData] = React.useState<number>(0);
const socket = React.useRef(null);
React.useEffect(() => {
if (typeof window !== undefined) {
socket.current = new WebSocket('wss://realtime.react-hot-toast.com/');
socket.current.onopen = (e) => {
socket.current.send(
JSON.stringify({
type: 'subscribe',
data: {
token: token,
events: [event],
},
})
);
};
socket.current.onmessage = (e) => {
const d = JSON.parse(e.data);
setData(d.count);
};
return () => {};
}
}, []);
return data;
};
export const SplitbeeCounter = () => {
const count = useSplitbeeCount('Trigger Toast', 'NTV7AYBLEXW3');
const letters = count.toString().split('');
return (
<div className="flex items-center justify-center p-4 flex-col gap-3 mt-4">
<div className="font-semibold text-toast-900 rounded text-lg">
Toasts made on this website so far
</div>
<div
className={clsx('grid gap-2 grid-flow-col', count === 0 && 'opacity-0')}
>
{letters.map((l, i) => (
<div
className={clsx(
'animate-custom-enter',
'bg-toast-100 rounded p-4 text-lg font-bold font-mono'
)}
key={i + '-' + l}
>
{l}
</div>
))}
</div>
<div className="text-toast-600">
⚡️ Real-time analytics by{' '}
<a
className="underline"
data-splitbee-event="Click Splitbee Analytics"
data-splitbee-event-location="Counter"
href="https://splitbee.io/?ref=rht-realtime"
>
Splitbee
</a>
</div>
</div>
);
};
================================================
FILE: site/components/sections/toast-example.tsx
================================================
import React, { useState } from 'react';
import toast from 'react-hot-toast';
import { EmojiButton } from '../emoji-button';
import { Code } from '../code';
const examples: Array<{
title: string;
action: () => void;
emoji: string;
snippet: string;
}> = [
{
title: 'Success',
emoji: '✅',
snippet: "toast.success('Successfully toasted!')",
action: () => {
toast.success('Successfully toasted!');
},
},
{
title: 'Error',
emoji: '❌',
snippet: `toast.error("This didn't work.")`,
action: () => {
toast.error("This didn't work.");
},
},
{
title: 'Promise',
emoji: '⏳',
snippet: `toast.promise(
saveSettings(settings),
{
loading: 'Saving...',
success: <b>Settings saved!</b>,
error: <b>Could not save.</b>,
}
);`,
action: () => {
const promise = new Promise((res, rej) => {
setTimeout(Math.random() > 0.5 ? res : rej, 1000);
});
toast.promise(
promise,
{
loading: 'Saving...',
success: <b>Settings saved!</b>,
error: <b>Could not save.</b>,
},
{
style: {
width: '200px',
paddingRight: '10px',
},
}
);
},
},
{
title: 'Multi Line',
emoji: '↕️',
snippet: `toast(
"This toast is super big. I don't think anyone could eat it in one bite.\\n\\nIt's larger than you expected. You eat it but it does not seem to get smaller.",
{
duration: 6000,
}
);`,
action: () => {
toast(
"This toast is super big. I don't think anyone could eat it in one bite.\n\n It's larger than you expected. You eat it but it does not seem to get smaller.",
{
duration: 6000,
}
);
},
},
{
title: 'Emoji',
emoji: '👏',
snippet: `toast('Good Job!', {
icon: '👏',
});`,
action: () => {
toast('Good Job!', {
icon: '👏',
});
},
},
{
title: 'Dark Mode',
emoji: '🌚',
snippet: `toast('Hello Darkness!',
{
icon: '👏',
style: {
borderRadius: '10px',
background: '#333',
color: '#fff',
},
}
);`,
action: () => {
toast('Hello Darkness!', {
icon: '👏',
style: {
borderRadius: '200px',
background: '#333',
color: '#fff',
},
});
},
},
{
title: 'JSX Content',
emoji: '🔩',
snippet: `toast((t) => (
<span>
Custom and <b>bold</b>
<button onClick={() => toast.dismiss(t.id)}>
Dismiss
</button>
</span>
));`,
action: () => {
toast((t) => (
<span>
Custom and <b>bold</b>
<button
className="ml-2 py-1 rounded px-2 border bg-gray-100 text-gray-900"
onClick={() => toast.dismiss(t.id)}
>
Dismiss
</button>
</span>
));
},
},
{
title: 'Themed',
emoji: '🎨',
snippet: `toast.success('Look at my styles.', {
style: {
border: '1px solid #713200',
padding: '16px',
color: '#713200',
},
iconTheme: {
primary: '#713200',
secondary: '#FFFAEE',
},
});`,
action: () => {
toast.success('Look at my styles.', {
style: {
border: '1px solid #713200',
padding: '16px',
color: '#713200',
},
iconTheme: {
primary: '#713200',
secondary: '#FFFAEE',
},
});
},
},
{
title: 'Custom Position',
emoji: '⬆️',
snippet: `toast.success('Always at the bottom.', {
position: "bottom-center"
})`,
action: () => {
toast.success('Always at the bottom.', {
position: 'bottom-center',
duration: 10000,
});
},
},
{
title: 'TailwindCSS',
emoji: '️💨',
snippet: `toast.custom((t) => (
<div
className={\`\${
t.visible ? 'animate-custom-enter' : 'animate-custom-leave'
} max-w-md w-full bg-white shadow-lg rounded-lg pointer-events-auto flex ring-1 ring-black ring-opacity-5\`}
>
<div className="flex-1 w-0 p-4">
<div className="flex items-start">
<div className="flex-shrink-0 pt-0.5">
<img
className="h-10 w-10 rounded-full"
src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixqx=6GHAjsWpt9&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2.2&w=160&h=160&q=80"
alt=""
/>
</div>
<div className="ml-3 flex-1">
<p className="text-sm font-medium text-gray-900">
Emilia Gates
</p>
<p className="mt-1 text-sm text-gray-500">
Sure! 8:30pm works great!
</p>
</div>
</div>
</div>
<div className="flex border-l border-gray-200">
<button
onClick={() => toast.dismiss(t.id)}
className="w-full border border-transparent rounded-none rounded-r-lg p-4 flex items-center justify-center text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
Close
</button>
</div>
</div>
))`,
action: () => {
// toast.custom(<TestApp />);
toast.custom(
(t) => (
<div
className={`${
t.visible ? 'animate-custom-enter' : 'animate-custom-leave'
} max-w-md w-full bg-white shadow-lg rounded-lg pointer-events-auto flex ring-1 ring-black ring-opacity-5`}
>
<div className="flex-1 w-0 p-4">
<div className="flex items-start">
<div className="flex-shrink-0 pt-0.5">
<img
className="h-10 w-10 rounded-full"
src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixqx=6GHAjsWpt9&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2.2&w=160&h=160&q=80"
alt=""
/>
</div>
<div className="ml-3 flex-1">
<p className="text-sm font-medium text-gray-900">
Emilia Gates
</p>
<p className="mt-1 text-sm text-gray-500">
Sure! 8:30pm works great!
</p>
</div>
</div>
</div>
<div className="flex border-l border-gray-200">
<button
onClick={() => toast.dismiss(t.id)}
className="w-full border border-transparent rounded-none rounded-r-lg p-4 flex items-center justify-center text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
Close
</button>
</div>
</div>
),
{
duration: 10000,
}
);
},
},
];
export const ToastExample = () => {
const [snippet, setSnippet] = useState(examples[0].snippet);
return (
<section className="grid md:grid-cols-2 gap-4">
<div className="flex items-center">
<div className="w-full grid grid-cols-2 gap-2 bg-toast-100 rounded-xl p-4">
{examples.map((e) => (
<EmojiButton
key={e.title}
emoji={e.emoji}
onClick={() => {
if (e.snippet) {
setSnippet(e.snippet);
}
(window as any).splitbee?.track('Trigger Toast', {
example: e.title,
});
e.action();
}}
>
{e.title}
</EmojiButton>
))}
</div>
</div>
<div className="md:h-72 w-full overflow-auto rounded-lg">
<Code snippet={snippet} className="!h-auto min-h-full" />
</div>
</section>
);
};
================================================
FILE: site/components/sections/toaster-example.tsx
================================================
import clsx from 'clsx';
import toast, { ToastPosition } from 'react-hot-toast';
import Arrow from '../../assets/arrow.svg';
import { Code } from '../code';
import { EmojiButton } from '../emoji-button';
export const positions: Array<ToastPosition> = [
'top-left',
'top-center',
'top-right',
'bottom-left',
'bottom-center',
'bottom-right',
];
export const ToasterExample: React.FC<{
position: ToastPosition;
onPosition: (pos: ToastPosition) => void;
reverse: boolean;
onReverse: (rev: boolean) => void;
}> = ({ position, onPosition, reverse, onReverse }) => {
const reverseIt = () => {
setTimeout(() => {
toast('Notification 1', {
icon: '1️⃣',
id: 'reverse-1',
});
}, 10);
setTimeout(
() =>
toast('Notification 2', {
icon: '2️⃣',
id: 'reverse-2',
}),
250
);
setTimeout(
() =>
toast('Notification 3', {
icon: '3️⃣',
id: 'reverse-3',
}),
500
);
setTimeout(
() =>
toast('Notification 4', {
icon: '4️⃣',
id: 'reverse-4',
}),
750
);
(window as any).splitbee?.track('Change Order', {
reverseOrder: !reverse,
});
onReverse(!reverse);
};
const renderPosition = (p: ToastPosition) => (
<button
id="p"
className={clsx(
'rounded-xl text-center text-xs md:text-sm py-2 px- flex items-center justify-center cursor-pointer flex-col md:flex-row',
position === p
? 'bg-toast-900 text-toast-100 '
: 'bg-white shadow-small-button'
)}
key={p}
onClick={() => {
toast.success(
<span>
Position set to <b>{p}</b>
</span>,
{
id: 'position',
}
);
(window as any).splitbee?.track('Change Position', {
position: p,
});
(window as any).splitbee?.track('Trigger Toast', {
example: 'position',
});
onPosition(p);
}}
>
<span className="mr-2">{p}</span>
</button>
);
return (
<section className="flex flex-col md:grid grid-cols-1 md:grid-cols-3 gap-2">
<Code
snippet={`<Toaster
position="${position}"
reverseOrder={${reverse}}
/>`}
/>
<div className="order-first md:order-none col-span-2 grid grid-cols-3 justify-between bg-toast-100 rounded-xl gap-x-2 gap-y-4 p-2 md:p-4">
{positions.map((p) => renderPosition(p))}
</div>
<div className="col-start-2 col-span-2 flex justify-center my-4">
<EmojiButton
emoji={
<Arrow
className={clsx(
'transform transition-transform',
((position.includes('bottom') && !reverse) ||
(position.includes('top') && reverse)) &&
'rotate-180'
)}
/>
}
onClick={reverseIt}
>
Toggle Direction
</EmojiButton>
</div>
</section>
);
};
================================================
FILE: site/next-env.d.ts
================================================
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
================================================
FILE: site/next.config.mjs
================================================
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';
import nextMdx from '@next/mdx';
const withMDX = nextMdx({
extension: /\.mdx?$/,
options: {
rehypePlugins: [rehypeSlug],
remarkPlugins: [remarkGfm],
providerImportSource: '@mdx-js/react',
},
});
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ['ts', 'tsx', 'md', 'mdx'],
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
});
return config;
},
async rewrites() {
return [
{
source: '/bee.js',
destination: 'https://cdn.splitbee.io/sb.js',
},
{
source: '/_hive/:slug',
destination: 'https://hive.splitbee.io/:slug',
},
];
},
};
export default withMDX(nextConfig);
================================================
FILE: site/package.json
================================================
{
"name": "site",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@mdx-js/loader": "^2.3.0",
"@mdx-js/react": "^2.3.0",
"@next/mdx": "^12.3.4",
"@svgr/webpack": "^6.5.1",
"@types/prismjs": "^1.26.5",
"@vercel/analytics": "^0.1.11",
"clsx": "^1.1.1",
"next": "^12.3.4",
"next-seo": "^5.15.0",
"postcss": "^8.4.49",
"prism-react-renderer": "^1.3.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "link:../",
"rehype-slug": "^5.1.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.15",
"@types/node": "^18.19.68",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"autoprefixer": "^10.4.20",
"remark-gfm": "^3.0.1",
"tailwindcss": "^3.4.17",
"typescript": "^4.9.5"
}
}
================================================
FILE: site/pages/_app.tsx
================================================
import '../styles/tailwind-utils.css';
import '../styles/main.css';
import * as React from 'react';
import Link from 'next/link';
import Head from 'next/head';
import { Analytics } from '@vercel/analytics/react';
import { MDXProvider } from '@mdx-js/react';
import { Code } from '../components/code';
const components = {
a: (props) => (
<Link href={props.href}>
<a {...props} />
</Link>
),
h1: (props) => {
const id = props.id || '';
return (
<h1 {...props}>
<Link href={`#${id}`}>
<a
className={`!no-underline !font-extrabold !text-toast-900 *:!text-toast-900`}
>
{props.children}
</a>
</Link>
</h1>
);
},
h2: (props) => {
const id = props.id || '';
return (
<h2 {...props}>
<Link href={`#${id}`}>
<a
className={`!no-underline !font-semibold !text-toast-800 *:!text-toast-800`}
>
{props.children}
</a>
</Link>
</h2>
);
},
h3: (props) => {
const id = props.id || '';
return (
<h3 {...props}>
<Link href={`#${id}`}>
<a
className={`!no-underline !font-semibold !text-toast-800 *:!text-toast-800`}
>
{props.children}
</a>
</Link>
</h3>
);
},
code: (props) =>
props.className ? (
<Code className={props.className} snippet={props.children} />
) : (
<code
className="bg-toast-300 py-1 my-0.5 px-1 rounded bg-opacity-40"
{...props}
/>
),
};
function MyApp({ Component, pageProps }) {
return (
<>
<Head>
{process.browser && (
<script async data-no-cookie data-api="/_hive" src="/bee.js" />
)}
<link rel="shortcut icon" href="/favicon.png" type="image/x-icon" />
</Head>
<MDXProvider components={components}>
<Component {...pageProps} />
<Analytics />
</MDXProvider>
</>
);
}
export default MyApp;
================================================
FILE: site/pages/docs/index.mdx
================================================
import Layout from '../../components/docs-layout';
import toast from 'react-hot-toast';
export const meta = {
title: 'Documentation',
};
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;
# Getting Started
Add beautiful notifications to your React app with [react-hot-toast](https://github.com/timolins/react-hot-toast).
### Install with pnpm
```sh
pnpm add react-hot-toast
```
### Install with NPM
```sh
npm install react-hot-toast
```
## Basic usage
```jsx
import toast, { Toaster } from 'react-hot-toast';
const notify = () => toast('Here is your toast.');
const App = () => {
return (
<div>
<button onClick={notify}>Make me a toast</button>
<Toaster />
</div>
);
};
```
================================================
FILE: site/pages/docs/multi-toaster.mdx
================================================
import Layout from '../../components/docs-layout';
import toast, { Toaster } from 'react-hot-toast';
export const meta = {
title: 'Multiple Toasters',
};
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;
# Multiple Toasters
React Hot Toast supports multiple toaster instances in your app, They can be used and configured independently of each other. This is useful for having notifications in different areas of your app.
You can use multiple toasters by creating a [`Toaster`](/docs/toaster) with a unique `toasterId`:
```jsx
<Toaster toasterId="sidebar" />
```
## Example
This example shows two toasters, each maintaining their own state and configuration.
<div className="not-prose flex gap-4 flex-col md:flex-row my-4">
<div className="relative min-h-[200px] bg-toast-200 text-toast-800 rounded-lg p-4 overflow-hidden flex-1 flex flex-col gap-2">
<p className="text-lg flex-1 text-center text-toast-300 flex items-center justify-center">Area 1</p>
<Toaster
toasterId="area1"
position="top-center"
containerStyle={{ position: 'absolute' }}
/>
<button
onClick={() => toast('Notification for Area 1', { toasterId: 'area1' })}
className="bg-toast-600 text-white px-4 py-2 rounded-lg hover:bg-toast-600 w-full"
>
Show Toast in Area 1
</button>
</div>
<div className="relative min-h-[200px] rounded-lg p-4 overflow-hidden flex-1 flex flex-col gap-2" style={{ backgroundColor: 'rgba(154, 134, 253, 0.15)' }}>
<p className="text-lg flex-1 text-center text-[#876fff84] flex items-center justify-center">Area 2</p>
<Toaster
toasterId="area2"
position="top-center"
containerStyle={{ position: 'absolute' }}
toastOptions={{
className: '!text-white px-4 py-2 border !rounded-full',
style: {
backgroundColor: 'rgb(154, 134, 253)',
borderColor: 'rgba(154, 134, 253, 0.3)'
}
}}
/>
<button
onClick={() => toast('Notification for Area 2', { toasterId: 'area2' })}
className="text-white px-4 py-2 rounded-lg bg-[#9a86fd] w-full"
>
Show Toast in Area 2
</button>
</div>
</div>
## Basic Usage
You can create multiple toasters providing unique `toasterId` to each `<Toaster />` component:
```jsx
// Create a toaster with a unique id
<Toaster toasterId="area1" />
// Create another toaster with a unique id
<Toaster toasterId="area2" toastOptions={{ ... }} />
```
To create a toast in a specific toaster, you can pass the `toasterId` to the `toast` function.
```jsx
// Create a toast in area 1
toast('Notification for Area 1', {
toasterId: 'area1',
});
```
When no `toasterId` is provided, it uses `"default"` as the `toasterId`.
### Positioning the toaster
When placing a toaster in a specific area of your app, set the position to `absolute` and the parent element to `relative`.
```jsx
<div style={{ position: 'relative' }}>
<Toaster
toasterId="area1"
position="top-center"
containerStyle={{ position: 'absolute' }}
/>
</div>
```
================================================
FILE: site/pages/docs/styling.mdx
================================================
import Layout from '../../components/docs-layout';
import toast from 'react-hot-toast';
export const meta = {
title: 'Styling',
};
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;
# Styling
You can style your notifications globally with the `toastOptions` inside the Toaster component, or for each notification manually.
### Set default for all toasts
```jsx
<Toaster
toastOptions={{
className: '',
style: {
border: '1px solid #713200',
padding: '16px',
color: '#713200',
},
}}
/>
```
### Set default for specific types
```jsx
<Toaster
toastOptions={{
success: {
style: {
background: 'green',
},
},
error: {
style: {
background: 'red',
},
},
}}
/>
```
### Style per toast
```jsx
toast('I have a border.', {
style: {
border: '1px solid black',
},
});
```
## Change the offset
If you want to change the offset of your notifications, you can adapt the absolute position in `containerStyle`.
```jsx
<Toaster
containerStyle={{
top: 20,
left: 20,
bottom: 20,
right: 20,
}}
/>
```
## Change position of the toaster
By default, the toaster is position fixed in the window. If you want to place it somewhere else, you can overwrite the position with `containerStyle`.
```jsx
<Toaster
containerStyle={{
position: 'relative',
}}
/>
```
## Change offset between toasts
If you want to change the offset between notifications change the gutter.
```jsx
<Toaster gutter={24} />
```
## Change icon color
All icon colors can be changed by supplying a `iconTheme` with a `primary` & `secondary` color.
```jsx
<Toaster
toastOptions={{
success: {
iconTheme: {
primary: 'green',
secondary: 'black',
},
},
}}
/>
```
## Change enter and exit animations
In this example, we provide a render function with the default `<ToastBar />`. We overwrite the animation style based on the current state.
```jsx
import { Toaster, ToastBar } from 'react-hot-toast';
<Toaster>
{(t) => (
<ToastBar
toast={t}
style={{
...t.style,
animation: t.visible
? 'custom-enter 1s ease'
: 'custom-exit 1s ease forwards',
}}
/>
)}
</Toaster>;
```
================================================
FILE: site/pages/docs/toast-bar.mdx
================================================
import Layout from '../../components/docs-layout';
import toast from 'react-hot-toast';
export const meta = {
title: '<ToastBar/> API',
};
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;
# `<ToastBar />` API
This is the **default toast component** rendered by the [Toaster](/docs/toaster). You can use this component in a [Toaster](/docs/toaster) with a custom render function to overwrite its defaults.
## Available options
```jsx
<ToastBar
toast={t}
style={{}} // Overwrite styles
position="top-center" // Used to adapt the animation
/>
```
## Add custom content
You can add a **render function to the ToastBar to modify its content**. An object containing The `icon` as well as the `message` are passed into the function.
### Add a dismiss button
In this example we add a basic dismiss button to all toasts, except if the loading one.
```jsx
import { toast, Toaster, ToastBar } from 'react-hot-toast';
<Toaster>
{(t) => (
<ToastBar toast={t}>
{({ icon, message }) => (
<>
{icon}
{message}
{t.type !== 'loading' && (
<button onClick={() => toast.dismiss(t.id)}>X</button>
)}
</>
)}
</ToastBar>
)}
</Toaster>;
```
================================================
FILE: site/pages/docs/toast.mdx
================================================
import Layout from '../../components/docs-layout';
import toast from 'react-hot-toast';
export const meta = {
title: 'toast() API',
};
# `toast()` API
Call it to create a toast from anywhere, even outside React. Make sure you add the [`<Toaster/>`](/docs/toaster) component to your app first.
## Available toast options
You can provide `ToastOptions` as the second argument. They will overwrite all options received from [`<Toaster/>`](/docs/toaster).
```js
toast('Hello World', {
duration: 4000,
position: 'top-center',
// Styling
style: {},
className: '',
// Custom Icon
icon: '👏',
// Change colors of success/error/loading icon
iconTheme: {
primary: '#000',
secondary: '#fff',
},
// Aria
ariaProps: {
role: 'status',
'aria-live': 'polite',
},
// Additional Configuration
removeDelay: 1000,
// Toaster instance
toasterId: 'default',
});
```
## Creating a toast
### Blank
```js
toast('Hello World');
```
The most basic variant. It does not have an icon by default, but you can provide one via the options. If you don't want any default styles, use `toast.custom()` instead.
### Success
```js
toast.success('Successfully created!');
```
Creates a notification with an animated checkmark. It can be themed with the `iconTheme` option.
### Error
```js
toast.error('This is an error!');
```
Creates a notification with an animated error icon. It can be themed with the `iconTheme` option.
### Custom (JSX)
```js
toast.custom(<div>Hello World</div>);
```
Creates a custom notification with JSX without default styles.
### Loading
```js
toast.loading('Waiting...');
```
This will create a loading notification. Most likely, you want to update it afterwards. For a friendly alternative, check out `toast.promise()`, which takes care of that automatically.
### Promise
This shorthand is useful for mapping a promise to a toast. It will update automatically when the promise resolves or fails.
#### Simple Usage
```js
const myPromise = fetchData();
toast.promise(myPromise, {
loading: 'Loading',
success: 'Got the data',
error: 'Error when fetching',
});
```
It's recommend to add min-width to your `toast.promise()` calls to **prevent jumps** from different message lengths.
#### Advanced
You can provide a function to the success/error messages to incorporate the result/error of the promise. The third argument are `toastOptions` similiar to [`<Toaster />`](/docs/toaster)
```js
toast.promise(
myPromise,
{
loading: 'Loading',
success: (data) => `Successfully saved ${data.name}`,
error: (err) => `This just happened: ${err.toString()}`,
},
{
style: {
minWidth: '250px',
},
success: {
duration: 5000,
icon: '🔥',
},
}
);
```
#### Using an Async Function
You can also provide a function that returns a promise, which will be called automatically.
```js
toast.promise(
async () => {
const { id } = await fetchData1();
await fetchData2(id);
},
{
loading: 'Loading',
success: 'Got the data',
error: 'Error when fetching',
}
);
```
## Default durations
Every type has its own duration. You can overwrite them `duration` with the toast options. This can be done per toast options or globally by the [`<Toaster/>`](/docs/toaster).
| type | duration |
| --------- | -------- |
| `blank` | 4000 |
| `error` | 4000 |
| `success` | 2000 |
| `custom` | 4000 |
| `loading` | Infinity |
### Dismiss toast programmatically
You can manually dismiss a notification with `toast.dismiss`. Be aware that it triggers the exit animation and does not remove the Toast instantly. Toasts will auto-remove after 1 second by default.
#### Dismiss a single toast
```js
const toastId = toast.loading('Loading...');
// ...
toast.dismiss(toastId);
```
You can dismiss all toasts at once, by leaving out the `toastId`.
#### Dismiss all toasts at once
```js
toast.dismiss();
```
To remove toasts instantly without any animations, use `toast.remove`.
#### Configure remove delay
```js
toast.success('Successfully created!', { removeDelay: 500 });
```
By default, the remove operation is delayed by 1000ms. This is how long a toast should be kept in the DOM after being dismissed. It is used to play the exit animation. This duration (number in milliseconds) can be configured when calling the toast.
Or, for all toasts, using the Toaster like so:
```js
<Toaster
toastOptions={{
removeDelay: 500,
}}
/>
```
#### Remove toasts instantly
```js
toast.remove(toastId);
// or
toast.remove();
```
### Update an existing toast
Each toast call returns a unique id. Use in the toast options to update the existing toast.
```js
const toastId = toast.loading('Loading...');
// ...
toast.success('This worked', {
id: toastId,
});
```
### Prevent duplicate toasts
To prevent duplicates of the same kind, you can provide a unique permanent id.
```js
toast.success('Copied to clipboard!', {
id: 'clipboard',
});
```
### Render JSX custom content
You can provide a React component instead of text. If you don't want any default styles use `toast.custom()` instead.
```jsx
toast(
<span>
Custom and <b>bold</b>
</span>,
{
icon: <Icon />,
}
);
```
You can also supply a function that receives the `Toast` as an argument, giving you access to all properties. This allows you to access the toast id, which can be used to add a dismiss button.
```jsx
toast(
(t) => (
<span>
Custom and <b>bold</b>
<button onClick={() => toast.dismiss(t.id)}>Dismiss</button>
</span>
),
{
icon: <Icon />,
}
);
```
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;
================================================
FILE: site/pages/docs/toaster.mdx
================================================
import Layout from '../../components/docs-layout';
import toast from 'react-hot-toast';
export const meta = {
title: '<Toaster/> API',
};
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;
# `<Toaster />` API
This component will render all toasts. Alternatively you can create own renderer with the headless [`useToaster()`](/docs/use-toaster) hook.
## Available options
```jsx
<Toaster
position="top-center"
reverseOrder={false}
gutter={8}
containerClassName=""
containerStyle={{}}
toasterId="default"
toastOptions={{
// Define default options
className: '',
duration: 5000,
removeDelay: 1000,
style: {
background: '#363636',
color: '#fff',
},
// Default options for specific types
success: {
duration: 3000,
iconTheme: {
primary: 'green',
secondary: 'black',
},
},
}}
/>
```
### `position` Prop
You can change the position of all toasts by modifying supplying `positon` prop.
| Positions | | |
| ----------- | ------------- | ------------ |
| top-left | top-center | top-right |
| bottom-left | bottom-center | bottom-right |
### `reverseOrder` Prop
Toasts spawn at top by default. Set to `true` if you want new toasts at the end.
### `containerClassName` Prop
Add a custom CSS class name to toaster div. Defaults to `undefined`.
### `containerStyle` Prop
Customize the style of toaster div. This can be used to change the offset of all toasts
### `gutter` Prop
Changes the gap between each toast. Defaults to `8`.
### `toasterId` Prop
You can change the toasterId to have a different toaster instance. Learn more about [multiple toasters](/docs/multi-toaster). Defaults to `"default"`.
### `toastOptions` Prop
These will act as default options for all toasts. See [`toast()`](/docs/toast) for all available options.
#### Type specific options
You can change the defaults for a specific type by adding, `success: {}`, `error: {}`, `loading: {}` or `custom: {}`.
## Using a custom render function
You can provide your **own render function** to the Toaster by passing it as children. It will be called for each [Toast](https://github.com/timolins/react-hot-toast/blob/main/src/core/types.ts#L34) allowing you to render any component based on the toast state.
### Minimal example
```jsx
import { Toaster, resolveValue } from 'react-hot-toast';
// In your app
<Toaster>
{(t) => (
<div
style={{ opacity: t.visible ? 1 : 0, background: 'white', padding: 8 }}
>
{resolveValue(t.message, t)}
</div>
)}
</Toaster>;
```
`resolveValue()` is needed to resolve all message types: Text, JSX or a function that resolves to JSX.
### Adapting the default [`<ToastBar/>`](/docs/toast-bar)
You can use this API to modify the default ToastBar as well. In this example we overwrite the animation style based on the current state.
```jsx
import { Toaster, ToastBar } from 'react-hot-toast';
<Toaster>
{(t) => (
<ToastBar
toast={t}
style={{
...t.style,
animation: t.visible
? 'custom-enter 1s ease'
: 'custom-exit 1s ease forwards',
}}
/>
)}
</Toaster>;
```
Check out the [`<ToastBar/>`](/docs/toast-bar) docs for more options.
================================================
FILE: site/pages/docs/use-toaster-store.mdx
================================================
import Layout from '../../components/docs-layout';
import toast from 'react-hot-toast';
export const meta = {
title: 'useToasterStore() API',
};
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;
# `useToasterStore()` API
This hook gives you access to the internal toaster state. This is the right choice if you need access to the data without wanting to roll your own toaster.
In comparison to [`useToaster()`](/docs/use-toaster) it does not handle pausing or provide handlers for creating your own notification system.
```jsx
import { useToasterStore } from 'react-hot-toast';
const { toasts, pausedAt } = useToasterStore();
```
================================================
FILE: site/pages/docs/use-toaster.mdx
================================================
import Layout from '../../components/docs-layout';
import toast from 'react-hot-toast';
export const meta = {
title: 'useToaster() API',
};
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;
# `useToaster()` API
The `useToaster()` hook provides a **headless toast management system** for building custom notification UIs. It manages toast state and lifecycle without rendering any components.
It handles pausing on hover, auto-removal, and provides a 1-second removal delay with `visible` flag for smooth animations.
**Alternative**: Use [`useToasterStore()`](/docs/use-toaster-store) if you already have a toaster instance and only need the state.
### Importing
```jsx
import { useToaster } from 'react-hot-toast';
```
You can also import from the headless entry point to exclude UI components:
```jsx
import { useToaster } from 'react-hot-toast/headless';
```
**Note**: [React Hot Toast 2.0](/docs/version-2) includes **custom render functions** for easier custom components.
## API Reference
### Parameters
```tsx
useToaster(
toastOptions?: DefaultToastOptions,
toasterId?: string
)
```
| Parameter | Type | Default | Description |
| -------------- | --------------------- | ----------- | ----------------------------------------------- |
| `toastOptions` | `DefaultToastOptions` | `undefined` | Default options for all toasts in this instance |
| `toasterId` | `string` | `'default'` | Unique identifier for this toaster instance |
### Returns
```tsx
{
toasts: Toast[];
handlers: {
startPause: () => void;
endPause: () => void;
updateHeight: (toastId: string, height: number) => void;
calculateOffset: (toast: Toast, options?: OffsetOptions) => number;
};
}
```
#### `toasts`
Array of all toasts in this toaster instance, including hidden ones for animation purposes.
#### `handlers`
- **`startPause()`**: Pause all toast timers (useful for hover states)
- **`endPause()`**: Resume all toast timers
- **`updateHeight(toastId, height)`**: Update toast height for offset calculations
- **`calculateOffset(toast, options)`**: Calculate vertical offset for toast positioning
## Multiple Toasters
You can create multiple independent toaster instances by providing a unique `toasterId`. See the [Multiple Toasters](/docs/multi-toaster) guide for detailed examples.
```jsx
const sidebar = useToaster({ duration: 5000 }, 'sidebar');
toast('Sidebar notification', { toasterId: 'sidebar' });
```
## Examples
### Basic Implementation
```jsx
import toast, { useToaster } from 'react-hot-toast/headless';
const Notifications = () => {
const { toasts, handlers } = useToaster();
const { startPause, endPause } = handlers;
return (
<div onMouseEnter={startPause} onMouseLeave={endPause}>
{toasts
.filter((toast) => toast.visible)
.map((toast) => (
<div key={toast.id} {...toast.ariaProps}>
{toast.message}
</div>
))}
</div>
);
};
// Create toasts from anywhere
toast('Hello World');
```
### Animated Implementation
This example uses all `toasts` (including hidden ones) to enable smooth animations. The `toast.visible` property controls opacity, while the 1-second removal delay provides time for exit animations.
**Live Demo**: [CodeSandbox](https://codesandbox.io/s/react-hot-toast-usetoaster-headless-example-zw7op?file=/src/App.js)
```jsx
import { useToaster } from 'react-hot-toast/headless';
const AnimatedNotifications = () => {
const { toasts, handlers } = useToaster();
const { startPause, endPause, calculateOffset, updateHeight } = handlers;
return (
<div
style={{
position: 'fixed',
top: 8,
left: 8,
}}
onMouseEnter={startPause}
onMouseLeave={endPause}
>
{toasts.map((toast) => {
const offset = calculateOffset(toast, {
reverseOrder: false,
gutter: 8,
});
const ref = (el) => {
if (el && typeof toast.height !== 'number') {
const height = el.getBoundingClientRect().height;
updateHeight(toast.id, height);
}
};
return (
<div
key={toast.id}
ref={ref}
style={{
position: 'absolute',
width: '200px',
background: 'papayawhip',
transition: 'all 0.5s ease-out',
opacity: toast.visible ? 1 : 0,
transform: `translateY(${offset}px)`,
}}
{...toast.ariaProps}
>
{toast.message}
</div>
);
})}
</div>
);
};
```
## Usage with React Native
The headless API works perfectly with React Native. View the [React Native example](<https://snack.expo.io/@timo/react-hot-toast---usetoaster()---react-native>) for implementation details.
================================================
FILE: site/pages/docs/version-2.mdx
================================================
import Layout from '../../components/docs-layout';
import toast from 'react-hot-toast';
export const meta = {
title: 'react-hot-toast 2.0 changes',
};
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;
# What's new in react-hot-toast 2.0
This release is all about **flexibility**. It allows you to create the notification system of your dreams, even simpler. Before we dig deeper into the new APIs, check out what's included in this release:
<div className="">
<span style={{ margin: '10px' }}>✨</span> <a href="#introducing-toastcustom" className="font-semibold">toast.custom()</a> <span className="">- Dispatch components as toast</span>
<br />
<span style={{ margin: '10px' }}>👀</span>
<a href="#better-accessibility" className="font-semibold">Reduced-motion support</a> <span className="">- Automatically adapts to system preference</span>
<br />
<span style={{ margin: '10px' }}>🔀</span>
<a href="#per-toast-positioning" className="font-semibold">Individually position toasts</a> <span className=""> </span>
<br />
<span style={{ margin: '10px' }}>🧈</span>
<a href="#smoother-exit-animation" className="font-semibold">Smoother exit animations</a> <span className=""></span>
<br />
<span style={{ margin: '10px' }}>⚙️</span> <a href="#custom-renderer-api" className="font-semibold">Custom Renderer API</a> <span className="">- Supply your own render function</span>
</div>
As well as a many [other improvements and fixes](#changelog).
## Introducing `toast.custom()`
This new function allows you to **render any React component** on the fly. Pass in JSX, and it will add it to the notification stack. There are no default styles applied, giving you complete control.
This API makes it super easy to add [Tailwind UI Notifications](https://tailwindui.com/components/application-ui/overlays/notifications) to your React app.
```jsx
// Minimal Example
toast.custom(<div>Minimal Example</div>);
// Tailwind Example
toast.custom((t) => (
<div
className={`bg-white px-6 py-4 shadow-md rounded-full ${
t.visible ? 'animate-custom-enter' : 'animate-custom-leave'
}`}
>
Hello TailwindCSS! 👋
</div>
));
```
<div className="w-full relative">
<button
onClick={() =>
toast.custom((t) => (
<div
className={`bg-white px-6 py-4 shadow-md rounded-full ${
t.visible ? 'animate-custom-enter' : 'animate-custom-leave'
}`}
>Hello from TailwindCSS! 👋</div>
))
}
className="bg-toast-800 text-toast-100 whitespace-nowrap py-1 px-3 shadow-md rounded-lg absolute mt-[-4.5rem] -ml-2 transform -translate-x-full left-full">Run Example</button>
</div>
In the example above, we pass in a **function that returns JSX**. This allows us to access the current toast state and toggle between the enter and exit animation.
Instead of CSS keyframe animations, you can use TailwindCSS classes by wrapping it in the [Transition](https://headlessui.dev/react/transition) component from [@headlessui/react](https://headlessui.dev/).
## Better accessibility
The prefers reduced motion is now respected by default. If react-hot-toast detects this setting, it will use fade transitions instead of sliding.
## Smoother exit animation
The exit animation is now less hectic when you have multiple toasts stacked.
## Per toast positioning
From now on, it's possible to have toasts at multiple positions at once. Just add the `position` you want as option when dispatching a toast.
```jsx
toast.success('Always at the bottom', {
position: 'bottom-center',
});
```
<div className="w-full relative">
<button
className="bg-toast-800 text-toast-100 whitespace-nowrap py-1 px-3 shadow-md rounded-lg absolute mt-[-4.5rem] -ml-2 transform -translate-x-full left-full"
onClick={() => {
toast.success('Always at the bottom', {
position: 'bottom-center',
});
}}>Run Example</button>
</div>
## Relative positioning
You can now overwrite the default position of the toaster and place it anywhere you want.
```jsx
<Toaster containerStyle={{ position: 'absolute' }} />
```
## Simpler offset styling
There is now a `gutter` option to control the gap between toasts.
```jsx
<Toaster gutter={30} />
```
The offset is now controlled by the Toaster and can be changed by overwriting the `top`, `right`, `bottom` and `left` styles.
```jsx
<Toaster containerStyle={{ top: '8px' }} />
```
## Custom Renderer API
You can now use the [`<Toaster/>`](/docs/toaster#using-a-custom-render-function) to render your own components. Pass in a function that receives a [Toast](https://github.com/timolins/react-hot-toast/blob/main/src/core/types.ts#L34) as the first argument, allowing you to render whatever you please.
This is a great alternative if you are using [`useToaster()`](/docs/use-toaster) to render create custom notfications.
This API allows us to dynamically react to the current state of your toasts. This can be used to **change the default animations**, add **a custom dismiss button** or render a custom notification, like [TailwindUI Notifications](https://tailwindui.com/components/application-ui/overlays/notifications).
```jsx
import { toast, Toaster, ToastBar } from 'react-hot-toast';
const CustomToaster = () => (
<Toaster>
{(t) => (
<ToastBar toast={t}>
{({ icon, message }) => (
<>
{icon}
{message}
{t.type !== 'loading' && (
<button onClick={() => toast.dismiss(t.id)}>X</button>
)}
</>
)}
</ToastBar>
)}
</Toaster>
);
```
This example adapts the [ToastBar](/docs/toast-bar) with its new render function API. You can read more about the APIs in the [Toaster](/docs/toaster) & [ToastBar](/docs/toast-bar) docs.
## Available now
Get react-hot-toast 2.0 while it's hot. Upgrading from 1.0.0 should be seamless for most users.
```sh
pnpm add react-hot-toast
```
## The future and beyond
React Hot Toast got a lot more flexible with this version, laying the **foundation for future releases**. Thanks to everyone who helped out; much appreciated!
In the next releases, I plan to add the [most requested feature](https://github.com/timolins/react-hot-toast/issues/7): a dismiss button. As well as support for [custom toast types](https://github.com/timolins/react-hot-toast/issues/23).
---
## Changelog
### New
- Easier Customization
- Create your own toast renderer (without useToaster)
- Support for custom render function in Toaster
- Support for custom render function in ToastBar
- `toast.custom()` - Render custom one-off toasts. No default styling will be applied.
- Per toast positioning
- New exit animation
- Change the gutter between toasts with `<Toaster gutter={20} />`
- Support for relative positioning
- Respect reduce motion OS setting
- Create persistent toasts with `duration: Infinity`
### Breaking Changes
- Use the `top`, `right`, `bottom`, `left` to in `containerStyle` to change the offset, instead of margin
- Loading toasts no longer disappear after 30 seconds
- `role` & `ariaLive` got moved into `ariaProps`
- `useToaster()` no longer exposes `visibleToasts`
- No longer expose `dispatch`
================================================
FILE: site/pages/index.tsx
================================================
import { NextSeo } from 'next-seo';
import toast, {
Toaster,
useToasterStore,
ToastPosition,
} from 'react-hot-toast';
import React, { useState } from 'react';
import clsx from 'clsx';
import Link from 'next/link';
import Logo from '../assets/logo.svg';
import Butter1 from '../assets/butter-1.svg';
import Butter2 from '../assets/butter-2.svg';
import GitHub from '../assets/github.svg';
import Checkmark from '../assets/checkmark.svg';
import { ToastExample } from '../components/sections/toast-example';
import { Footer } from '../components/sections/footer';
import { ToasterExample } from '../components/sections/toaster-example';
import { SplitbeeCounter } from '../components/sections/splitbee-counter';
import packageInfo from '../../package.json';
const version = packageInfo.version;
const Feature: React.FC<{ children?: React.ReactNode }> = ({ children }) => (
<div className="flex gap-1 items-center">
<Checkmark />
<span className="font-bold">{children}</span>
</div>
);
const Step: React.FC<{
count: number;
title: string;
subTitle: string;
code: React.ReactElement;
}> = (props) => (
<div className="flex flex-col gap-1 items-center">
<div className="h-6 w-6 mb-2 text-sm rounded-full bg-toast-900 text-toast-50 flex items-center justify-center">
{props.count}
</div>
<div className="font-bold">{props.title}</div>
<div className="text-red-700 text-sm">{props.subTitle}</div>
<code className="mt-2 border border-toast-200 py-2 px-4 rounded font-bold bg-white w-full text-center">
{props.code}
</code>
</div>
);
const Steps = () => (
<div className="grid grid-cols-1 md:grid-cols-3 gap-x-4 gap-y-8 my-12">
<Step
count={1}
title="Install package"
subTitle="It weighs less than 5kb"
code={
<code>
<span className="text-toast-600">pnpm add</span>{' '}
<span className="text-toast-800">react-hot-toast</span>
</code>
}
></Step>
<Step
count={2}
title="Add Toaster to your app"
subTitle="Make sure it's placed at the top"
code={
<>
<span className="text-toast-600">{'<div>'}</span>
<span className="text-toast-800">{'<Toaster/>'}</span>
<span className="text-toast-600">{'</div>'}</span>
</>
}
></Step>
<Step
count={3}
title="Start toasting!"
subTitle="Call it from anywhere"
code={
<>
<span className="text-toast-600">{'toast'}</span>
<span className="text-toast-800">{'("Hello World")'}</span>
</>
}
></Step>
</div>
);
const Features = () => (
<div className="my-12 grid gap-x-8 gap-y-5 grid-cols-2 md:grid-cols-3">
<Feature>Hot by default</Feature>
<Feature>Easy to use</Feature>
<Feature>Accessible</Feature>
<Feature>Emoji Support</Feature>
<Feature>Customizable</Feature>
<Feature>Promise API</Feature>
<Feature>Lightweight</Feature>
<Feature>Pause on hover</Feature>
<Feature>Headless Hooks</Feature>
</div>
);
export default function Home() {
const [position, setPosition] = useState<ToastPosition>('top-center');
const [reverse, setReverse] = useState(false);
const { toasts: allToasts } = useToasterStore();
const shouldFade =
allToasts.filter((t) => t.visible).length && position.includes('top');
return (
<div className="overflow-x-hidden">
<NextSeo
title={'react-hot-toast - The Best React Notifications in Town'}
openGraph={{
images: [
{
url: `https://react-hot-toast.com/social-image.png`,
width: 1200,
height: 630,
},
],
}}
description="Add beautiful notifications to your React app with react-hot-toast. Lightweight. Smoking hot by default."
/>
<header className="bg-gradient-to-b from-toast-50 to-white bg-opacity-10">
<div className="container flex flex-col items-center relative">
<Butter1
className="absolute -left-24 md:left-24 transition-opacity duration-200"
style={{
opacity: shouldFade ? 0.5 : 1,
}}
/>
<div>
<Logo
role="img"
aria-label="react-hot-toast"
className="relative animate-slide-in transition-all duration-200 -mt-8 md:-mt-4"
style={{
willChange: 'filter',
opacity: shouldFade ? 0.2 : 1,
filter: `blur(${shouldFade ? 6 : 0}px)`,
}}
/>
</div>
<div className="text-center my-12 relative duration-200">
<h1 className="text-3xl md:text-4xl animate-custom-enter font-bold text-toast-900">
The Best Toast in Town.
</h1>
<h2 className="text-xl md:text-2xl font-bold text-toast-600 mt-2">
Smoking hot React notifications.
</h2>
</div>
<div className="grid md:grid-cols-2 gap-4 rounded-2xl bg-toast-200 p-4 w-full max-w-lg">
<button
data-splitbee-event="Trigger Toast"
data-splitbee-event-example="CTA"
className={clsx(
'rounded-lg font-bold gap-4 flex bg-gradient-to-b from-white to-toast-200 shadow-button text-center',
'py-4 px-6',
'active:translate-y-0.5 active:shadow-button-active active:bg-gray-100 transform',
'focus:outline-none focus:ring-4'
)}
style={{
transitionProperty: 'box-shadow, transform',
}}
onClick={() => {
const promise = new Promise((res, rej) => {
if (Math.random() < 0.85) {
setTimeout(res, 1000);
} else {
setTimeout(rej, 3000);
}
});
toast.promise(
promise,
{
loading: 'Preparing toast',
error: 'Whoops, it burnt',
success: "Here's your toast",
},
{
style: {
width: '200px',
paddingRight: '10px',
},
}
);
}}
>
<div>🛎 </div>
<span className="flex-1 mr-2">Make me a toast</span>
</button>
<a
className={clsx(
'rounded-lg flex font-bold bg-white py-4 px-6 shadow-button text-toast-800',
'active:translate-y-0.5 active:shadow-button-active transform'
)}
style={{
transitionProperty: 'box-shadow, transform',
}}
data-splitbee-event="Open Link"
data-splitbee-event-target="GitHub"
onClick={() => {}}
href="https://github.com/timolins/react-hot-toast"
>
<GitHub className="opacity-100" />
<span className="flex-1 text-toast-800 text-center">GitHub</span>
</a>
</div>
<div className="text-toast-600 my-2">
<Link href="/docs">
<a className="underline">Documentation</a>
</Link>
{' · '}
<span>v{version}</span>
</div>
<Features />
<Steps />
<div className="w-full max-w-4xl">
<div className="my-14">
<h2 className="ml-5 text-2xl font-bold">Examples</h2>
<ToastExample />
</div>
<div className="my-14">
<h2 className="ml-5 mr-5 mb-4 text-2xl font-bold">
Change Position
</h2>
<ToasterExample
onReverse={setReverse}
reverse={reverse}
position={position}
onPosition={setPosition}
/>
</div>
</div>
</div>
</header>
<SplitbeeCounter />
<Toaster position={position} reverseOrder={reverse} toastOptions={{}} />
<div className="container flex justify-end -mt-10 pointer-events-none">
<Butter2 className="transform translate-x-20" />
</div>
<Footer noBadge />
</div>
);
}
================================================
FILE: site/postcss.config.js
================================================
module.exports = {
plugins: ['tailwindcss'],
};
================================================
FILE: site/styles/main.css
================================================
@tailwind base;
@tailwind components;
html,
body,
body > div {
@apply flex flex-col justify-between flex-1 min-h-full text-toast-900;
}
button,
a {
@apply outline-none focus:outline-none ring-offset-green-900 ring-toast-900 ring-opacity-30 transition-shadow duration-100 focus:ring-4;
}
================================================
FILE: site/styles/prism-theme.css
================================================
/* Generated with http://k88hudson.github.io/syntax-highlighting-theme-generator/www */
/* http://k88hudson.github.io/react-markdocs */
/**
* @author k88hudson
*
* Based on prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
/*********************************************************
* General
*/
pre[class*="language-"],
code[class*="language-"] {
color: #a1724e;
font-size: 13px;
text-shadow: none;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::selection,
code[class*="language-"]::selection,
pre[class*="language-"]::mozselection,
code[class*="language-"]::mozselection {
text-shadow: none;
background: #ffff00;
}
@media print {
pre[class*="language-"],
code[class*="language-"] {
text-shadow: none;
}
}
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
background: #faf0de;
}
:not(pre) > code[class*="language-"] {
padding: .1em .3em;
border-radius: .3em;
color: #db4c69;
background: #f9f2f4;
}
/*********************************************************
* Tokens
*/
.namespace {
opacity: .7;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #dddddd;
}
.token.punctuation {
color: #999999;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #327015;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #54b427;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #8c4913;
background: transparent;
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #482307;
}
.token.function {
color: #381b05;
}
.token.regex,
.token.important,
.token.variable {
color: #ffedc0;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
/*********************************************************
* Line highlighting
*/
pre[data-line] {
position: relative;
}
pre[class*="language-"] > code[class*="language-"] {
position: relative;
z-index: 1;
}
.line-highlight {
position: absolute;
left: 0;
right: 0;
padding: inherit 0;
margin-top: 1em;
background: #ffe092;
box-shadow: inset 5px 0 0 #482307;
z-index: 0;
pointer-events: none;
line-height: inherit;
white-space: pre;
}
================================================
FILE: site/styles/tailwind-utils.css
================================================
@tailwind utilities;
================================================
FILE: site/tailwind.config.js
================================================
module.exports = {
mode: 'jit',
content: [
'./pages/*.tsx',
'./pages/**/*.tsx',
'./pages/*.mdx',
'./pages/**/*.mdx',
'./components/*.tsx',
'./components/**/*.tsx',
],
theme: {
extend: {
boxShadow: {
'small-button': '0px 1px 2px rgba(126, 56, 0, 0.5)',
button:
'-6px 8px 10px rgba(81, 41, 10, 0.1), 0px 2px 2px rgba(81, 41, 10, 0.2)',
'button-active':
'-1px 2px 5px rgba(81, 41, 10, 0.15), 0px 1px 1px rgba(81, 41, 10, 0.15)',
},
animation: {
'custom-enter': 'custom-enter 200ms ease-out',
'custom-leave': 'custom-leave 150ms ease-in forwards',
'slide-in': 'slide-in 1.2s cubic-bezier(.41,.73,.51,1.02)',
},
keyframes: {
'custom-enter': {
'0%': { transform: 'scale(0.9)', opacity: 0 },
'100%': { transform: 'scale(1)', opacity: 1 },
},
'custom-leave': {
'0%': { transform: 'scale(1)', opacity: 1 },
'100%': { transform: 'scale(0.9)', opacity: 0 },
},
'slide-in': {
'0%': { transform: 'translateY(-100%)' },
'100%': { transform: 'translateY(0)' },
},
},
colors: {
toast: {
'50': '#FFF6DF',
'100': '#fdf7f1',
'200': '#F8EEDB',
'300': '#ebbf99',
'400': '#dea373',
'500': '#ce864f',
'600': '#A1724E',
'700': '#8c501c',
'800': '#5c340f',
'900': '#482307',
},
},
typography: (theme) => ({
DEFAULT: {
css: {
'--tw-prose-bullets': theme('colors.toast[400]'),
'--tw-prose-links': theme('colors.toast[600]'),
color: theme('colors.toast.900'),
h1: {
color: theme('colors.toast.900'),
},
h2: {
color: theme('colors.toast.900'),
},
h3: {
color: theme('colors.toast.800'),
},
h4: {
color: theme('colors.toast.900'),
},
a: {
color: theme('colors.toast.600'),
},
strong: {
color: theme('colors.toast.900'),
},
pre: {
color: null,
backgroundColor: null,
overflowX: 'auto',
fontSize: theme('fontSize.base'),
padding: 0,
},
'pre pre': {
padding: theme('spacing.4'),
margin: 0,
},
'pre code': {
backgroundColor: 'transparent',
borderWidth: '0',
borderRadius: '0',
fontWeight: '400',
color: 'inherit',
fontFamily: 'inherit',
lineHeight: 'inherit',
},
code: {
color: theme('colors.toast.900'),
fontWeight: '600',
},
'code::before': {
content: '""',
},
'code::after': {
content: '""',
},
thead: {
color: theme('colors.toast.900'),
fontWeight: '600',
borderBottomWidth: '1px',
borderBottomColor: theme('colors.toast.200'),
},
'tbody tr': {
borderBottomWidth: '1px',
borderBottomColor: theme('colors.toast.200'),
},
'ul > li::before': {
content: '""',
position: 'absolute',
backgroundColor: theme('colors.toast.800'),
borderRadius: '50%',
},
// ...
},
},
}),
},
container: {
padding: '1rem',
center: true,
},
},
variants: {
extend: {
translate: ['active'],
gradientColorStops: ['active'],
boxShadow: ['active'],
},
},
plugins: [require('@tailwindcss/typography')],
};
================================================
FILE: site/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": [
"next-env.d.ts",
"types/*.d.ts",
"**/*.ts",
"**/*.tsx",
"**/**/*.ts",
"**/**/*.tsx",
"../**/*.ts",
"../**/*.tsx"
],
"exclude": [
"node_modules"
]
}
================================================
FILE: site/types/mdx.d.ts
================================================
declare module '*.mdx' {
let MDXComponent: (props: any) => React.ReactElement;
export default MDXComponent;
}
================================================
FILE: site/types/svg.d.ts
================================================
interface SvgrComponent
extends React.StatelessComponent<React.SVGAttributes<SVGElement>> {}
declare module '*.svg' {
const value: SvgrComponent;
export default value;
}
================================================
FILE: src/components/checkmark.tsx
================================================
import { styled, keyframes } from 'goober';
const circleAnimation = keyframes`
from {
transform: scale(0) rotate(45deg);
opacity: 0;
}
to {
transform: scale(1) rotate(45deg);
opacity: 1;
}`;
const checkmarkAnimation = keyframes`
0% {
height: 0;
width: 0;
opacity: 0;
}
40% {
height: 0;
width: 6px;
opacity: 1;
}
100% {
opacity: 1;
height: 10px;
}`;
export interface CheckmarkTheme {
primary?: string;
secondary?: string;
}
export const CheckmarkIcon = styled('div')<CheckmarkTheme>`
width: 20px;
opacity: 0;
height: 20px;
border-radius: 10px;
background: ${(p) => p.primary || '#61d345'};
position: relative;
transform: rotate(45deg);
animation: ${circleAnimation} 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)
forwards;
animation-delay: 100ms;
&:after {
content: '';
box-sizing: border-box;
animation: ${checkmarkAnimation} 0.2s ease-out forwards;
opacity: 0;
animation-delay: 200ms;
position: absolute;
border-right: 2px solid;
border-bottom: 2px solid;
border-color: ${(p) => p.secondary || '#fff'};
bottom: 6px;
left: 6px;
height: 10px;
width: 6px;
}
`;
================================================
FILE: src/components/error.tsx
================================================
import { styled, keyframes } from 'goober';
const circleAnimation = keyframes`
from {
transform: scale(0) rotate(45deg);
opacity: 0;
}
to {
transform: scale(1) rotate(45deg);
opacity: 1;
}`;
const firstLineAnimation = keyframes`
from {
transform: scale(0);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}`;
const secondLineAnimation = keyframes`
from {
transform: scale(0) rotate(90deg);
opacity: 0;
}
to {
transform: scale(1) rotate(90deg);
opacity: 1;
}`;
export interface ErrorTheme {
primary?: string;
secondary?: string;
}
export const ErrorIcon = styled('div')<ErrorTheme>`
width: 20px;
opacity: 0;
height: 20px;
border-radius: 10px;
background: ${(p) => p.primary || '#ff4b4b'};
position: relative;
transform: rotate(45deg);
animation: ${circleAnimation} 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)
forwards;
animation-delay: 100ms;
&:after,
&:before {
content: '';
animation: ${firstLineAnimation} 0.15s ease-out forwards;
animation-delay: 150ms;
position: absolute;
border-radius: 3px;
opacity: 0;
background: ${(p) => p.secondary || '#fff'};
bottom: 9px;
left: 4px;
height: 2px;
width: 12px;
}
&:before {
animation: ${secondLineAnimation} 0.15s ease-out forwards;
animation-delay: 180ms;
transform: rotate(90deg);
}
`;
================================================
FILE: src/components/loader.tsx
================================================
import { styled, keyframes } from 'goober';
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
export interface LoaderTheme {
primary?: string;
secondary?: string;
}
export const LoaderIcon = styled('div')<LoaderTheme>`
width: 12px;
height: 12px;
box-sizing: border-box;
border: 2px solid;
border-radius: 100%;
border-color: ${(p) => p.secondary || '#e0e0e0'};
border-right-color: ${(p) => p.primary || '#616161'};
animation: ${rotate} 1s linear infinite;
`;
================================================
FILE: src/components/toast-bar.tsx
================================================
import * as React from 'react';
import { styled, keyframes } from 'goober';
import { Toast, ToastPosition, resolveValue, Renderable } from '../core/types';
import { ToastIcon } from './toast-icon';
import { prefersReducedMotion } from '../core/utils';
const enterAnimation = (factor: number) => `
0% {transform: translate3d(0,${factor * -200}%,0) scale(.6); opacity:.5;}
100% {transform: translate3d(0,0,0) scale(1); opacity:1;}
`;
const exitAnimation = (factor: number) => `
0% {transform: translate3d(0,0,-1px) scale(1); opacity:1;}
100% {transform: translate3d(0,${factor * -150}%,-1px) scale(.6); opacity:0;}
`;
const fadeInAnimation = `0%{opacity:0;} 100%{opacity:1;}`;
const fadeOutAnimation = `0%{opacity:1;} 100%{opacity:0;}`;
// Use :where() for zero specificity - allows Tailwind to override easily
const ToastBarBase = styled('div')`
:where(&) {
display: flex;
align-items: center;
background: #fff;
color: #363636;
line-height: 1.3;
will-change: transform;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1), 0 3px 3px rgba(0, 0, 0, 0.05);
max-width: 350px;
pointer-events: auto;
padding: 8px 10px;
border-radius: 8px;
}
`;
const Message = styled('div')`
display: flex;
justify-content: center;
margin: 4px 10px;
color: inherit;
flex: 1 1 auto;
white-space: pre-line;
`;
interface ToastBarProps {
toast: Toast;
position?: ToastPosition;
style?: React.CSSProperties;
children?: (components: {
icon: Renderable;
message: Renderable;
}) => Renderable;
}
const getAnimationStyle = (
position: ToastPosition,
visible: boolean
): React.CSSProperties => {
const top = position.includes('top');
const factor = top ? 1 : -1;
const [enter, exit] = prefersReducedMotion()
? [fadeInAnimation, fadeOutAnimation]
: [enterAnimation(factor), exitAnimation(factor)];
return {
animation: visible
? `${keyframes(enter)} 0.35s cubic-bezier(.21,1.02,.73,1) forwards`
: `${keyframes(exit)} 0.4s forwards cubic-bezier(.06,.71,.55,1)`,
};
};
export const ToastBar: React.FC<ToastBarProps> = React.memo(
({ toast, position, style, children }) => {
const animationStyle: React.CSSProperties = toast.height
? getAnimationStyle(
toast.position || position || 'top-center',
toast.visible
)
: { opacity: 0 };
const icon = <ToastIcon toast={toast} />;
const message = (
<Message {...toast.ariaProps}>
{resolveValue(toast.message, toast)}
</Message>
);
return (
<ToastBarBase
className={toast.className}
style={{
...animationStyle,
...style,
...toast.style,
}}
>
{typeof children === 'function' ? (
children({
icon,
message,
})
) : (
<>
{icon}
{message}
</>
)}
</ToastBarBase>
);
}
);
================================================
FILE: src/components/toast-icon.tsx
================================================
import * as React from 'react';
import { styled, keyframes } from 'goober';
import { Toast } from '../core/types';
import { ErrorIcon, ErrorTheme } from './error';
import { LoaderIcon, LoaderTheme } from './loader';
import { CheckmarkIcon, CheckmarkTheme } from './checkmark';
const StatusWrapper = styled('div')`
position: absolute;
`;
const IndicatorWrapper = styled('div')`
position: relative;
display: flex;
justify-content: center;
align-items: center;
min-width: 20px;
min-height: 20px;
`;
const enter = keyframes`
from {
transform: scale(0.6);
opacity: 0.4;
}
to {
transform: scale(1);
opacity: 1;
}`;
export const AnimatedIconWrapper = styled('div')`
position: relative;
transform: scale(0.6);
opacity: 0.4;
min-width: 20px;
animation: ${enter} 0.3s 0.12s cubic-bezier(0.175, 0.885, 0.32, 1.275)
forwards;
`;
export type IconThemes = Partial<{
success: CheckmarkTheme;
error: ErrorTheme;
loading: LoaderTheme;
}>;
export const ToastIcon: React.FC<{
toast: Toast;
}> = ({ toast }) => {
const { icon, type, iconTheme } = toast;
if (icon !== undefined) {
if (typeof icon === 'string') {
return <AnimatedIconWrapper>{icon}</AnimatedIconWrapper>;
} else {
return icon;
}
}
if (type === 'blank') {
return null;
}
return (
<IndicatorWrapper>
<LoaderIcon {...iconTheme} />
{type !== 'loading' && (
<StatusWrapper>
{type === 'error' ? (
<ErrorIcon {...iconTheme} />
) : (
<CheckmarkIcon {...iconTheme} />
)}
</StatusWrapper>
)}
</IndicatorWrapper>
);
};
================================================
FILE: src/components/toaster.tsx
================================================
import { css, setup } from 'goober';
import * as React from 'react';
import {
resolveValue,
ToasterProps,
ToastPosition,
ToastWrapperProps,
} from '../core/types';
import { useToaster } from '../core/use-toaster';
import { prefersReducedMotion } from '../core/utils';
import { ToastBar } from './toast-bar';
setup(React.createElement);
const ToastWrapper = ({
id,
className,
style,
onHeightUpdate,
children,
}: ToastWrapperProps) => {
const ref = React.useCallback(
(el: HTMLElement | null) => {
if (el) {
const updateHeight = () => {
const height = el.getBoundingClientRect().height;
onHeightUpdate(id, height);
};
updateHeight();
new MutationObserver(updateHeight).observe(el, {
subtree: true,
childList: true,
characterData: true,
});
}
},
[id, onHeightUpdate]
);
return (
<div ref={ref} className={className} style={style}>
{children}
</div>
);
};
const getPositionStyle = (
position: ToastPosition,
offset: number
): React.CSSProperties => {
const top = position.includes('top');
const verticalStyle: React.CSSProperties = top ? { top: 0 } : { bottom: 0 };
const horizontalStyle: React.CSSProperties = position.includes('center')
? {
justifyContent: 'center',
}
: position.includes('right')
? {
justifyContent: 'flex-end',
}
: {};
return {
left: 0,
right: 0,
display: 'flex',
position: 'absolute',
transition: prefersReducedMotion()
? undefined
: `all 230ms cubic-bezier(.21,1.02,.73,1)`,
transform: `translateY(${offset * (top ? 1 : -1)}px)`,
...verticalStyle,
...horizontalStyle,
};
};
const activeClass = css`
z-index: 9999;
> * {
pointer-events: auto;
}
`;
const DEFAULT_OFFSET = 16;
export const Toaster: React.FC<ToasterProps> = ({
reverseOrder,
position = 'top-center',
toastOptions,
gutter,
children,
toasterId,
containerStyle,
containerClassName,
}) => {
const { toasts, handlers } = useToaster(toastOptions, toasterId);
return (
<div
data-rht-toaster={toasterId || ''}
style={{
position: 'fixed',
zIndex: 9999,
top: DEFAULT_OFFSET,
left: DEFAULT_OFFSET,
right: DEFAULT_OFFSET,
bottom: DEFAULT_OFFSET,
pointerEvents: 'none',
...containerStyle,
}}
className={containerClassName}
onMouseEnter={handlers.startPause}
onMouseLeave={handlers.endPause}
>
{toasts.map((t) => {
const toastPosition = t.position || position;
const offset = handlers.calculateOffset(t, {
reverseOrder,
gutter,
defaultPosition: position,
});
const positionStyle = getPositionStyle(toastPosition, offset);
return (
<ToastWrapper
id={t.id}
key={t.id}
onHeightUpdate={handlers.updateHeight}
className={t.visible ? activeClass : ''}
style={positionStyle}
>
{t.type === 'custom' ? (
resolveValue(t.message, t)
) : children ? (
children(t)
) : (
<ToastBar toast={t} position={toastPosition} />
)}
</ToastWrapper>
);
})}
</div>
);
};
================================================
FILE: src/core/store.ts
================================================
import { useEffect, useState, useRef } from 'react';
import { DefaultToastOptions, Toast, ToastType } from './types';
export const TOAST_EXPIRE_DISMISS_DELAY = 1000;
export const TOAST_LIMIT = 20;
export const DEFAULT_TOASTER_ID = 'default';
interface ToasterSettings {
toastLimit: number;
}
export enum ActionType {
ADD_TOAST,
UPDATE_TOAST,
UPSERT_TOAST,
DISMISS_TOAST,
REMOVE_TOAST,
START_PAUSE,
END_PAUSE,
}
export type Action =
| {
type: ActionType.ADD_TOAST;
toast: Toast;
}
| {
type: ActionType.UPSERT_TOAST;
toast: Toast;
}
| {
type: ActionType.UPDATE_TOAST;
toast: Partial<Toast>;
}
| {
type: ActionType.DISMISS_TOAST;
toastId?: string;
}
| {
type: ActionType.REMOVE_TOAST;
toastId?: string;
}
| {
type: ActionType.START_PAUSE;
time: number;
}
| {
type: ActionType.END_PAUSE;
time: number;
};
interface ToasterState {
toasts: Toast[];
settings: ToasterSettings;
pausedAt: number | undefined;
}
interface State {
[toasterId: string]: ToasterState;
}
export const reducer = (state: ToasterState, action: Action): ToasterState => {
const { toastLimit } = state.settings;
switch (action.type) {
case ActionType.ADD_TOAST:
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, toastLimit),
};
case ActionType.UPDATE_TOAST:
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
};
case ActionType.UPSERT_TOAST:
const { toast } = action;
return reducer(state, {
type: state.toasts.find((t) => t.id === toast.id)
? ActionType.UPDATE_TOAST
: ActionType.ADD_TOAST,
toast,
});
case ActionType.DISMISS_TOAST:
const { toastId } = action;
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
dismissed: true,
visible: false,
}
: t
),
};
case ActionType.REMOVE_TOAST:
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
case ActionType.START_PAUSE:
return {
...state,
pausedAt: action.time,
};
case ActionType.END_PAUSE:
const diff = action.time - (state.pausedAt || 0);
return {
...state,
pausedAt: undefined,
toasts: state.toasts.map((t) => ({
...t,
pauseDuration: t.pauseDuration + diff,
})),
};
}
};
const listeners: Array<
[toasterId: string, reducer: (state: ToasterState) => void]
> = [];
const defaultToasterState: ToasterState = {
toasts: [],
pausedAt: undefined,
settings: {
toastLimit: TOAST_LIMIT,
},
};
let memoryState: State = {};
export const dispatch = (action: Action, toasterId = DEFAULT_TOASTER_ID) => {
memoryState[toasterId] = reducer(
memoryState[toasterId] || defaultToasterState,
action
);
listeners.forEach(([id, listener]) => {
if (id === toasterId) {
listener(memoryState[toasterId]);
}
});
};
export const dispatchAll = (action: Action) =>
Object.keys(memoryState).forEach((toasterId) => dispatch(action, toasterId));
export const getToasterIdFromToastId = (toastId: string) =>
Object.keys(memoryState).find((toasterId) =>
memoryState[toasterId].toasts.some((t) => t.id === toastId)
);
export const createDispatch =
(toasterId = DEFAULT_TOASTER_ID) =>
(action: Action) => {
dispatch(action, toasterId);
};
export const defaultTimeouts: {
[key in ToastType]: number;
} = {
blank: 4000,
error: 4000,
success: 2000,
loading: Infinity,
custom: 4000,
};
export const useStore = (
toastOptions: DefaultToastOptions = {},
toasterId: string = DEFAULT_TOASTER_ID
): ToasterState => {
const [state, setState] = useState<ToasterState>(
memoryState[toasterId] || defaultToasterState
);
const initial = useRef(memoryState[toasterId]);
// TODO: Switch to useSyncExternalStore when targeting React 18+
useEffect(() => {
if (initial.current !== memoryState[toasterId]) {
setState(memoryState[toasterId]);
}
listeners.push([toasterId, setState]);
return () => {
const index = listeners.findIndex(([id]) => id === toasterId);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [toasterId]);
const mergedToasts = state.toasts.map((t) => ({
...toastOptions,
...toastOptions[t.type],
...t,
removeDelay:
t.removeDelay ||
toastOptions[t.type]?.removeDelay ||
toastOptions?.removeDelay,
duration:
t.duration ||
toastOptions[t.type]?.duration ||
toastOptions?.duration ||
defaultTimeouts[t.type],
style: {
...toastOptions.style,
...toastOptions[t.type]?.style,
...t.style,
},
}));
return {
...state,
toasts: mergedToasts,
};
};
================================================
FILE: src/core/toast.ts
================================================
import {
Renderable,
Toast,
ToastOptions,
ToastType,
DefaultToastOptions,
ValueOrFunction,
resolveValue,
} from './types';
import { genId } from './utils';
import {
createDispatch,
Action,
ActionType,
dispatchAll,
getToasterIdFromToastId,
} from './store';
type Message = ValueOrFunction<Renderable, Toast>;
type ToastHandler = (message: Message, options?: ToastOptions) => string;
const createToast = (
message: Message,
type: ToastType = 'blank',
opts?: ToastOptions
): Toast => ({
createdAt: Date.now(),
visible: true,
dismissed: false,
type,
ariaProps: {
role: 'status',
'aria-live': 'polite',
},
message,
pauseDuration: 0,
...opts,
id: opts?.id || genId(),
});
const createHandler =
(type?: ToastType): ToastHandler =>
(message, options) => {
const toast = createToast(message, type, options);
const dispatch = createDispatch(
toast.toasterId || getToasterIdFromToastId(toast.id)
);
dispatch({ type: ActionType.UPSERT_TOAST, toast });
return toast.id;
};
const toast = (message: Message, opts?: ToastOptions) =>
createHandler('blank')(message, opts);
toast.error = createHandler('error');
toast.success = createHandler('success');
toast.loading = createHandler('loading');
toast.custom = createHandler('custom');
/**
* Dismisses the toast with the given id. If no id is given, dismisses all toasts.
* The toast will transition out and then be removed from the DOM.
* Applies to all toasters, except when a `toasterId` is given.
*/
toast.dismiss = (toastId?: string, toasterId?: string) => {
const action: Action = {
type: ActionType.DISMISS_TOAST,
toastId,
};
if (toasterId) {
createDispatch(toasterId)(action);
} else {
dispatchAll(action);
}
};
/**
* Dismisses all toasts.
*/
toast.dismissAll = (toasterId?: string) => toast.dismiss(undefined, toasterId);
/**
* Removes the toast with the given id.
* The toast will be removed from the DOM without any transition.
*/
toast.remove = (toastId?: string, toasterId?: string) => {
const action: Action = {
type: ActionType.REMOVE_TOAST,
toastId,
};
if (toasterId) {
createDispatch(toasterId)(action);
} else {
dispatchAll(action);
}
};
/**
* Removes all toasts.
*/
toast.removeAll = (toasterId?: string) => toast.remove(undefined, toasterId);
/**
* Create a loading toast that will automatically updates with the promise.
*/
toast.promise = <T>(
promise: Promise<T> | (() => Promise<T>),
msgs: {
loading: Renderable;
success?: ValueOrFunction<Renderable, T>;
error?: ValueOrFunction<Renderable, any>;
},
opts?: DefaultToastOptions
) => {
const id = toast.loading(msgs.loading, { ...opts, ...opts?.loading });
if (typeof promise === 'function') {
promise = promise();
}
promise
.then((p) => {
const successMessage = msgs.success
? resolveValue(msgs.success, p)
: undefined;
if (successMessage) {
toast.success(successMessage, {
id,
...opts,
...opts?.success,
});
} else {
toast.dismiss(id);
}
return p;
})
.catch((e) => {
const errorMessage = msgs.error ? resolveValue(msgs.error, e) : undefined;
if (errorMessage) {
toast.error(errorMessage, {
id,
...opts,
...opts?.error,
});
} else {
toast.dismiss(id);
}
});
return promise;
};
export { toast };
================================================
FILE: src/core/types.ts
================================================
import { CSSProperties } from 'react';
export type ToastType = 'success' | 'error' | 'loading' | 'blank' | 'custom';
export type ToastPosition =
| 'top-left'
| 'top-center'
| 'top-right'
| 'bottom-left'
| 'bottom-center'
| 'bottom-right';
export type Renderable = React.ReactElement | string | null;
export interface IconTheme {
primary: string;
secondary: string;
}
export type ValueFunction<TValue, TArg> = (arg: TArg) => TValue;
export type ValueOrFunction<TValue, TArg> =
| TValue
| ValueFunction<TValue, TArg>;
const isFunction = <TValue, TArg>(
valOrFunction: ValueOrFunction<TValue, TArg>
): valOrFunction is ValueFunction<TValue, TArg> =>
typeof valOrFunction === 'function';
export const resolveValue = <TValue, TArg>(
valOrFunction: ValueOrFunction<TValue, TArg>,
arg: TArg
): TValue => (isFunction(valOrFunction) ? valOrFunction(arg) : valOrFunction);
export interface Toast {
type: ToastType;
id: string;
toasterId?: string;
message: ValueOrFunction<Renderable, Toast>;
icon?: Renderable;
duration?: number;
pauseDuration: number;
position?: ToastPosition;
removeDelay?: number;
ariaProps: {
role: 'status' | 'alert';
'aria-live': 'assertive' | 'off' | 'polite';
};
style?: CSSProperties;
className?: string;
iconTheme?: IconTheme;
createdAt: number;
visible: boolean;
dismissed: boolean;
height?: number;
}
export type ToastOptions = Partial<
Pick<
Toast,
| 'id'
| 'icon'
| 'duration'
| 'ariaProps'
| 'className'
| 'style'
| 'position'
| 'iconTheme'
| 'toasterId'
| 'removeDelay'
>
>;
export type DefaultToastOptions = ToastOptions & {
[key in ToastType]?: ToastOptions;
};
export interface ToasterProps {
position?: ToastPosition;
toastOptions?: DefaultToastOptions;
reverseOrder?: boolean;
gutter?: number;
containerStyle?: React.CSSProperties;
containerClassName?: string;
toasterId?: string;
children?: (toast: Toast) => React.ReactElement;
}
export interface ToastWrapperProps {
id: string;
className?: string;
style?: React.CSSProperties;
onHeightUpdate: (id: string, height: number) => void;
children?: React.ReactNode;
}
================================================
FILE: src/core/use-toaster.ts
================================================
import { useEffect, useCallback, useRef } from 'react';
import { createDispatch, ActionType, useStore, dispatch } from './store';
import { toast } from './toast';
import { DefaultToastOptions, Toast, ToastPosition } from './types';
export const REMOVE_DELAY = 1000;
export const useToaster = (
toastOptions?: DefaultToastOptions,
toasterId: string = 'default'
) => {
const { toasts, pausedAt } = useStore(toastOptions, toasterId);
const toastTimeouts = useRef(
new Map<Toast['id'], ReturnType<typeof setTimeout>>()
).current;
const addToRemoveQueue = useCallback(
(toastId: string, removeDelay = REMOVE_DELAY) => {
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: ActionType.REMOVE_TOAST,
toastId: toastId,
});
}, removeDelay);
toastTimeouts.set(toastId, timeout);
},
[]
);
useEffect(() => {
if (pausedAt) {
return;
}
const now = Date.now();
const timeouts = toasts.map((t) => {
if (t.duration === Infinity) {
return;
}
const durationLeft =
(t.duration || 0) + t.pauseDuration - (now - t.createdAt);
if (durationLeft < 0) {
if (t.visible) {
toast.dismiss(t.id);
}
return;
}
return setTimeout(() => toast.dismiss(t.id, toasterId), durationLeft);
});
return () => {
timeouts.forEach((timeout) => timeout && clearTimeout(timeout));
};
}, [toasts, pausedAt, toasterId]);
const dispatch = useCallback(createDispatch(toasterId), [toasterId]);
const startPause = useCallback(() => {
dispatch({
type: ActionType.START_PAUSE,
time: Date.now(),
});
}, [dispatch]);
const updateHeight = useCallback(
(toastId: string, height: number) => {
dispatch({
type: ActionType.UPDATE_TOAST,
toast: { id: toastId, height },
});
},
[dispatch]
);
const endPause = useCallback(() => {
if (pausedAt) {
dispatch({ type: ActionType.END_PAUSE, time: Date.now() });
}
}, [pausedAt, dispatch]);
const calculateOffset = useCallback(
(
toast: Toast,
opts?: {
reverseOrder?: boolean;
gutter?: number;
defaultPosition?: ToastPosition;
}
) => {
const { reverseOrder = false, gutter = 8, defaultPosition } = opts || {};
const relevantToasts = toasts.filter(
(t) =>
(t.position || defaultPosition) ===
(toast.position || defaultPosition) && t.height
);
const toastIndex = relevantToasts.findIndex((t) => t.id === toast.id);
const toastsBefore = relevantToasts.filter(
(toast, i) => i < toastIndex && toast.visible
).length;
const offset = relevantToasts
.filter((t) => t.visible)
.slice(...(reverseOrder ? [toastsBefore + 1] : [0, toastsBefore]))
.reduce((acc, t) => acc + (t.height || 0) + gutter, 0);
return offset;
},
[toasts]
);
// Keep track of dismissed toasts and remove them after the delay
useEffect(() => {
toasts.forEach((toast) => {
if (toast.dismissed) {
addToRemoveQueue(toast.id, toast.removeDelay);
} else {
// If toast becomes visible again, remove it from the queue
const timeout = toastTimeouts.get(toast.id);
if (timeout) {
clearTimeout(timeout);
toastTimeouts.delete(toast.id);
}
}
});
}, [toasts, addToRemoveQueue]);
return {
toasts,
handlers: {
updateHeight,
startPause,
endPause,
calculateOffset,
},
};
};
================================================
FILE: src/core/utils.ts
================================================
export const genId = (() => {
let count = 0;
return () => {
return (++count).toString();
};
})();
export const prefersReducedMotion = (() => {
// Cache result
let shouldReduceMotion: boolean | undefined = undefined;
return () => {
if (shouldReduceMotion === undefined && typeof window !== 'undefined') {
const mediaQuery = matchMedia('(prefers-reduced-motion: reduce)');
shouldReduceMotion = !mediaQuery || mediaQuery.matches;
}
return shouldReduceMotion;
};
})();
================================================
FILE: src/headless/index.ts
================================================
import { toast } from '../core/toast';
export type {
DefaultToastOptions,
IconTheme,
Renderable,
Toast,
ToasterProps,
ToastOptions,
ToastPosition,
ToastType,
ValueFunction,
ValueOrFunction,
} from '../core/types';
export { resolveValue } from '../core/types';
export { useToaster } from '../core/use-toaster';
export { useStore as useToasterStore } from '../core/store';
export { toast };
export default toast;
================================================
FILE: src/index.ts
================================================
import { toast } from './core/toast';
export * from './headless';
export { ToastBar } from './components/toast-bar';
export { ToastIcon } from './components/toast-icon';
export { Toaster } from './components/toaster';
export { CheckmarkIcon } from './components/checkmark';
export { ErrorIcon } from './components/error';
export { LoaderIcon } from './components/loader';
export { toast };
export default toast;
================================================
FILE: test/setup.ts
================================================
import '@testing-library/jest-dom';
// Mock matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
// Mock getBoundingClientRect
Element.prototype.getBoundingClientRect = jest.fn(() => {
return {
width: 300,
height: 120,
x: 0,
y: 0,
top: 0,
left: 0,
bottom: 0,
right: 0,
toJSON: () => '{}',
};
});
================================================
FILE: test/toast.test.tsx
================================================
import React, { useEffect, useState } from 'react';
import {
render,
screen,
act,
waitFor,
fireEvent,
} from '@testing-library/react';
import toast, { resolveValue, Toaster, ToastIcon } from '../src';
import { defaultTimeouts } from '../src/core/store';
import { REMOVE_DELAY } from '../src/core/use-toaster';
beforeEach(() => {
// Tests should run in serial for improved isolation
// To prevent collision with global state, reset all toasts for each test
toast.remove();
jest.useFakeTimers();
});
afterEach((done) => {
act(() => {
jest.runAllTimers();
jest.useRealTimers();
done();
});
});
const waitTime = (time: number) => {
act(() => {
jest.advanceTimersByTime(time);
});
};
const TOAST_DURATION = 1000;
test('close notification', async () => {
render(
<>
<button
type="button"
onClick={() => {
toast.success((t) => (
<div>
Example
<button
aria-hidden={!t.visible}
type="button"
onClick={() => {
toast.dismiss(t.id);
}}
title={'close'}
>
Close
</button>
</div>
));
}}
>
Notify!
</button>
<Toaster />
</>
);
fireEvent.click(screen.getByRole('button', { name: /Notify/i }));
await waitFor(() => screen.getByText(/example/i));
expect(screen.queryByText(/example/i)).toBeInTheDocument();
fireEvent.click(await screen.findByRole('button', { name: /close/i }));
waitTime(REMOVE_DELAY);
expect(screen.queryByText(/example/i)).not.toBeInTheDocument();
});
test('promise toast', async () => {
const WAIT_DELAY = 1000;
render(
<>
<button
type="button"
onClick={() => {
const sleep = new Promise((resolve) => {
setTimeout(resolve, WAIT_DELAY);
});
toast.promise(sleep, {
loading: 'Loading...',
success: 'Success!',
error: 'Error!',
});
}}
>
Notify!
</button>
<Toaster />
</>
);
act(() => {
fireEvent.click(screen.getByRole('button', { name: /Notify/i }));
});
await screen.findByText(/loading/i);
expect(screen.queryByText(/loading/i)).toBeInTheDocument();
waitTime(WAIT_DELAY);
await waitFor(() => {
expect(screen.queryByText(/success/i)).toBeInTheDocument();
});
});
test('promise toast error', async () => {
const WAIT_DELAY = 1000;
render(
<>
<button
type="button"
onClick={() => {
const sleep = new Promise((_, rej) => {
setTimeout(rej, WAIT_DELAY);
});
toast.promise(sleep, {
loading: 'Loading...',
success: 'Success!',
error: 'Error!',
});
}}
>
Notify!
</button>
<Toaster />
</>
);
act(() => {
fireEvent.click(screen.getByRole('button', { name: /Notify/i }));
});
await screen.findByText(/loading/i);
expect(screen.queryByText(/loading/i)).toBeInTheDocument();
waitTime(WAIT_DELAY);
await waitFor(() => {
expect(screen.queryByText(/error/i)).toBeInTheDocument();
});
});
test('error toast with custom duration', async () => {
render(
<>
<button
type="button"
onClick={() => {
toast.error('An error happened', {
duration: TOAST_DURATION,
});
}}
>
Notify!
</button>
<Toaster position="bottom-right" />
</>
);
act(() => {
fireEvent.click(screen.getByRole('button', { name: /Notify/i }));
});
await screen.findByText(/error/i);
expect(screen.queryByText(/error/i)).toBeInTheDocument();
waitTime(TOAST_DURATION);
waitTime(REMOVE_DELAY);
expect(screen.queryByText(/error/i)).not.toBeInTheDocument();
});
test('different toasts types with dismiss', async () => {
render(
<>
<Toaster />
</>
);
act(() => {
toast.success('Success!');
});
act(() => {
toast.error('Error!');
});
act(() => {
toast('Emoji Icon', {
icon: '✅',
});
});
act(() => {
toast('Custom Icon', {
icon: <span>ICON</span>,
});
});
let loadingToastId: string;
act(() => {
loadingToastId = toast.loading('Loading!');
});
expect(screen.queryByText(/error/i)).toBeInTheDocument();
expect(screen.queryByText(/success/i)).toBeInTheDocument();
expect(screen.queryByText(/loading/i)).toBeInTheDocument();
expect(screen.queryByText('✅')).toBeInTheDocument();
expect(screen.queryByText('ICON')).toBeInTheDocument();
waitTime(defaultTimeouts.success);
waitTime(REMOVE_DELAY);
expect(screen.queryByText(/success/i)).not.toBeInTheDocument();
expect(screen.queryByText(/error/i)).toBeInTheDocument();
waitTime(defaultTimeouts.error);
waitTime(REMOVE_DELAY);
expect(screen.queryByText(/error/i)).not.toBeInTheDocument();
act(() => {
toast.dismiss(loadingToastId);
});
waitTime(REMOVE_DELAY);
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
test('custom toaster renderer', async () => {
render(
<>
<Toaster>
{(t) => (
<div className="custom-toast">
<ToastIcon toast={t} />
{resolveValue(t.message, t)}
</div>
)}
</Toaster>
</>
);
act(() => {
toast.success('Success!');
});
expect(screen.queryByText(/success/i)).toHaveClass('custom-toast');
act(() => {
toast(<b>Bold</b>);
});
expect(screen.queryByText(/bold/i)).toBeInTheDocument();
act(() => {
toast.custom('Custom');
});
expect(screen.queryByText(/custom/i)).not.toHaveClass('custom-toast');
});
test('pause toast', async () => {
render(
<>
<Toaster>
{(t) => (
<div className="custom-toast">
<ToastIcon toast={t} />
{resolveValue(t.message, t)}
</div>
)}
</Toaster>
</>
);
act(() => {
toast.success('Hover me!', {
duration: 1000,
});
});
waitTime(500);
const toastElement = screen.getByText(/hover me/i);
expect(toastElement).toBeInTheDocument();
fireEvent.mouseEnter(toastElement);
waitTime(10000);
expect(toastElement).toBeInTheDocument();
fireEvent.mouseLeave(toastElement);
waitTime(1000);
waitTime(1000);
expect(toastElement).not.toBeInTheDocument();
});
test('"toast" can be called from useEffect hook', async () => {
const MyComponent = () => {
const [success, setSuccess] = useState(false);
useEffect(() => {
toast.success('Success toast');
setSuccess(true);
}, []);
return success ? <div>MyComponent finished</div> : null;
};
render(
<>
<MyComponent />
<Toaster />
</>
);
await screen.findByText(/MyComponent finished/i);
expect(screen.queryByText(/Success toast/i)).toBeInTheDocument();
});
describe('Multi-Toaster behavior', () => {
test('renders toasts in correct containers and dismisses them individually', () => {
render(
<>
<Toaster position="top-left" containerClassName="default-toaster" />
<Toaster
position="top-right"
toasterId="second-toaster"
containerClassName="second-toaster"
/>
<Toaster
position="bottom-center"
toasterId="third-toaster"
containerClassName="third-toaster"
/>
</>
);
// Show three toasts in three different toasters
act(() => {
toast.success('Default toaster message');
toast.error('Second toaster message', {
toasterId: 'second-toaster',
id: 'second-toast',
});
toast.loading('Third toaster message', { toasterId: 'third-toaster' });
});
const defaultContainer = document.querySelector('.default-toaster');
const secondContainer = document.querySelector('.second-toaster');
const thirdContainer = document.querySelector('.third-toaster');
// Ensure each toast is present and in the correct container
expect(defaultContainer).toContainElement(
screen.getByText('Default toaster message')
);
expect(secondContainer).toContainElement(
screen.getByText('Second toaster message')
);
expect(thirdContainer).toContainElement(
screen.getByText('Third toaster message')
);
// Dismiss only the toast in the second toaster
act(() => {
toast.dismiss('second-toast');
});
waitTime(REMOVE_DELAY);
expect(
screen.queryByText('Second toaster message')
).not.toBeInTheDocument();
expect(screen.queryByText('Default toaster message')).toBeInTheDocument();
expect(screen.queryByText('Third toaster message')).toBeInTheDocument();
// Dismiss all toasts
act(() => {
toast.dismissAll();
});
waitTime(REMOVE_DELAY);
expect(
screen.queryByText('Default toaster message')
).not.toBeInTheDocument();
expect(
screen.queryByText('Second toaster message')
).not.toBeInTheDocument();
expect(screen.queryByText('Third toaster message')).not.toBeInTheDocument();
});
test('updates a toast in a specific toaster without affecting others', () => {
render(
<>
<Toaster containerClassName="default-toaster" />
<Toaster
toasterId="updatable-toaster"
containerClassName="updatable-toaster"
/>
</>
);
let toastId: string;
// Create a loading toast in the second toaster
act(() => {
toastId = toast.loading('Please wait...', {
toasterId: 'updatable-toaster',
});
});
const secondContainer = document.querySelector('.updatable-toaster');
expect(secondContainer).toContainElement(
screen.getByText('Please wait...')
);
// Now update that toast to success
act(() => {
// Note that we are not providing a toasterId here
toast.success('Data saved!', {
id: toastId,
});
});
// Confirm the updated text
expect(screen.queryByText('Please wait...')).not.toBeInTheDocument();
expect(secondContainer).toContainElement(screen.getByText('Data saved!'));
});
test('dismisses all toasts from a specific toaster and leaves others intact', () => {
render(
<>
<Toaster containerClassName="default-toaster" />
<Toaster toasterId="other-toaster" containerClassName="other-toaster" />
</>
);
// Create one toast in each toaster
act(() => {
toast.success('Default toaster toast');
toast.success('Other toaster toast', { toasterId: 'other-toaster' });
});
// Ensure both appear
expect(screen.getByText('Default toaster toast')).toBeInTheDocument();
expect(screen.getByText('Other toaster toast')).toBeInTheDocument();
// Dismiss only the second toaster's toasts
act(() => {
toast.dismissAll('other-toaster');
});
waitTime(REMOVE_DELAY);
// The other toaster's toast should be gone, default remains
expect(screen.queryByText('Other toaster toast')).not.toBeInTheDocument();
expect(screen.queryByText('Default toaster toast')).toBeInTheDocument();
});
test('dismisses all toasts across all toasters with dismissAll', () => {
render(
<>
<Toaster containerClassName="default-toaster" />
<Toaster toasterId="other-toaster" containerClassName="other-toaster" />
</>
);
// Create one toast in each toaster
act(() => {
toast.success('Default toaster toast');
toast.error('Other toaster toast', { toasterId: 'other-toaster' });
});
// Dismiss every toast in all toasters
act(() => {
toast.dismissAll();
});
waitTime(REMOVE_DELAY);
// Both should be removed
expect(screen.queryByText('Default toaster toast')).not.toBeInTheDocument();
expect(screen.queryByText('Other toaster toast')).not.toBeInTheDocument();
});
test('removes toasts immediately when calling toast.remove()', () => {
render(
<>
<Toaster toasterId="instant-remove-toaster" />
<Toaster toasterId="another-toaster" />
</>
);
act(() => {
toast.success('Removable toast #1', {
toasterId: 'instant-remove-toaster',
});
toast.error('Removable toast #2', { toasterId: 'another-toaster' });
});
expect(screen.queryByText('Removable toast #1')).toBeInTheDocument();
expect(screen.queryByText('Removable toast #2')).toBeInTheDocument();
act(() => {
toast.removeAll('instant-remove-toaster');
});
expect(screen.queryByText('Removable toast #1')).not.toBeInTheDocument();
expect(screen.queryByText('Removable toast #2')).toBeInTheDocument();
});
});
================================================
FILE: tsconfig.json
================================================
{
// see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
"include": ["src", "types", "test"],
"compilerOptions": {
"module": "esnext",
"types": ["jest", "@testing-library/jest-dom"],
"lib": ["dom", "esnext"],
// output .d.ts declaration files for consumers
"declaration": true,
// output .js.map sourcemap files for consumers
"sourceMap": true,
// match output dir to input dir. e.g. dist/index instead of dist/src/index
"rootDir": "./src",
// stricter type-checking for stronger correctness. Recommended by TS
"strict": true,
// linter checks for common issues
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
// noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
"noUnusedParameters": true,
// use Node's module resolution algorithm, instead of the legacy TS one
"moduleResolution": "node",
// transpile JSX to React.createElement
"jsx": "react",
// interop between ESM and CJS modules. Recommended by TS
"esModuleInterop": true,
// significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
"skipLibCheck": true,
// error out if import and file system have a casing mismatch. Recommended by TS
"forceConsistentCasingInFileNames": true,
"noEmit": true,
}
}
================================================
FILE: tsup.config.ts
================================================
import { defineConfig, Options } from 'tsup';
import { minifyTemplates, writeFiles } from 'esbuild-minify-templates';
const commonConfig: Options = {
minify: true,
dts: true,
format: ['esm', 'cjs'],
sourcemap: true,
clean: true,
esbuildPlugins: [
minifyTemplates({ taggedOnly: false }),
writeFiles(),
],
};
export default defineConfig([
{
...commonConfig,
esbuildOptions: (options) => {
// Append "use client" to the top of the react entry point
options.banner = {
js: '"use client";',
};
},
entry: ['src/index.ts'],
outDir: 'dist',
},
{
...commonConfig,
entry: ['src/headless/index.ts'],
outDir: 'headless',
},
]);
gitextract_zguhd_ms/ ├── .github/ │ └── workflows/ │ ├── main.yml │ └── size.yml ├── .gitignore ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── site/ │ ├── .gitignore │ ├── README.md │ ├── components/ │ │ ├── code.tsx │ │ ├── docs-layout.tsx │ │ ├── emoji-button.tsx │ │ └── sections/ │ │ ├── footer.tsx │ │ ├── splitbee-counter.tsx │ │ ├── toast-example.tsx │ │ └── toaster-example.tsx │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── package.json │ ├── pages/ │ │ ├── _app.tsx │ │ ├── docs/ │ │ │ ├── index.mdx │ │ │ ├── multi-toaster.mdx │ │ │ ├── styling.mdx │ │ │ ├── toast-bar.mdx │ │ │ ├── toast.mdx │ │ │ ├── toaster.mdx │ │ │ ├── use-toaster-store.mdx │ │ │ ├── use-toaster.mdx │ │ │ └── version-2.mdx │ │ └── index.tsx │ ├── postcss.config.js │ ├── styles/ │ │ ├── main.css │ │ ├── prism-theme.css │ │ └── tailwind-utils.css │ ├── tailwind.config.js │ ├── tsconfig.json │ └── types/ │ ├── mdx.d.ts │ └── svg.d.ts ├── src/ │ ├── components/ │ │ ├── checkmark.tsx │ │ ├── error.tsx │ │ ├── loader.tsx │ │ ├── toast-bar.tsx │ │ ├── toast-icon.tsx │ │ └── toaster.tsx │ ├── core/ │ │ ├── store.ts │ │ ├── toast.ts │ │ ├── types.ts │ │ ├── use-toaster.ts │ │ └── utils.ts │ ├── headless/ │ │ └── index.ts │ └── index.ts ├── test/ │ ├── setup.ts │ └── toast.test.tsx ├── tsconfig.json └── tsup.config.ts
SYMBOL INDEX (36 symbols across 17 files)
FILE: site/components/docs-layout.tsx
function DocsLayout (line 27) | function DocsLayout({ meta, children }) {
FILE: site/components/sections/footer.tsx
function Footer (line 4) | function Footer({ noBadge }: { noBadge?: boolean }) {
FILE: site/next.config.mjs
method webpack (line 17) | webpack(config) {
method rewrites (line 24) | async rewrites() {
FILE: site/pages/_app.tsx
function MyApp (line 71) | function MyApp({ Component, pageProps }) {
FILE: site/pages/index.tsx
function Home (line 102) | function Home() {
FILE: site/types/svg.d.ts
type SvgrComponent (line 1) | interface SvgrComponent
FILE: src/components/checkmark.tsx
type CheckmarkTheme (line 29) | interface CheckmarkTheme {
FILE: src/components/error.tsx
type ErrorTheme (line 33) | interface ErrorTheme {
FILE: src/components/loader.tsx
type LoaderTheme (line 12) | interface LoaderTheme {
FILE: src/components/toast-bar.tsx
type ToastBarProps (line 47) | interface ToastBarProps {
FILE: src/components/toast-icon.tsx
type IconThemes (line 41) | type IconThemes = Partial<{
FILE: src/components/toaster.tsx
constant DEFAULT_OFFSET (line 83) | const DEFAULT_OFFSET = 16;
FILE: src/core/store.ts
constant TOAST_EXPIRE_DISMISS_DELAY (line 4) | const TOAST_EXPIRE_DISMISS_DELAY = 1000;
constant TOAST_LIMIT (line 5) | const TOAST_LIMIT = 20;
constant DEFAULT_TOASTER_ID (line 6) | const DEFAULT_TOASTER_ID = 'default';
type ToasterSettings (line 8) | interface ToasterSettings {
type ActionType (line 12) | enum ActionType {
type Action (line 22) | type Action =
type ToasterState (line 52) | interface ToasterState {
type State (line 58) | interface State {
FILE: src/core/toast.ts
type Message (line 19) | type Message = ValueOrFunction<Renderable, Toast>;
type ToastHandler (line 21) | type ToastHandler = (message: Message, options?: ToastOptions) => string;
FILE: src/core/types.ts
type ToastType (line 3) | type ToastType = 'success' | 'error' | 'loading' | 'blank' | 'custom';
type ToastPosition (line 4) | type ToastPosition =
type Renderable (line 12) | type Renderable = React.ReactElement | string | null;
type IconTheme (line 14) | interface IconTheme {
type ValueFunction (line 19) | type ValueFunction<TValue, TArg> = (arg: TArg) => TValue;
type ValueOrFunction (line 20) | type ValueOrFunction<TValue, TArg> =
type Toast (line 34) | interface Toast {
type ToastOptions (line 60) | type ToastOptions = Partial<
type DefaultToastOptions (line 76) | type DefaultToastOptions = ToastOptions & {
type ToasterProps (line 80) | interface ToasterProps {
type ToastWrapperProps (line 91) | interface ToastWrapperProps {
FILE: src/core/use-toaster.ts
constant REMOVE_DELAY (line 6) | const REMOVE_DELAY = 1000;
FILE: test/toast.test.tsx
constant TOAST_DURATION (line 35) | const TOAST_DURATION = 1000;
Condensed preview — 55 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (130K chars).
[
{
"path": ".github/workflows/main.yml",
"chars": 518,
"preview": "name: CI\non: [push]\njobs:\n build:\n name: Build & test\n runs-on: ubuntu-latest\n steps:\n - uses: actions/ch"
},
{
"path": ".github/workflows/size.yml",
"chars": 422,
"preview": "name: size\non: [pull_request]\njobs:\n size:\n runs-on: ubuntu-latest\n env:\n CI_JOB_NUMBER: 1\n steps:\n "
},
{
"path": ".gitignore",
"chars": 76,
"preview": "*.log\n.DS_Store\nnode_modules\n.cache\ncoverage\ndist\n/headless\n.vscode\n.vercel\n"
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2020 Timo Lins\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
},
{
"path": "README.md",
"chars": 1940,
"preview": "<a href=\"https://react-hot-toast.com/\"><img alt=\"react-hot-toast - Try it out\" src=\"https://github.com/timolins/react-ho"
},
{
"path": "jest.config.js",
"chars": 187,
"preview": "/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */\nmodule.exports = {\n preset: 'ts-jest',\n testEnvironme"
},
{
"path": "package.json",
"chars": 2565,
"preview": "{\n \"name\": \"react-hot-toast\",\n \"description\": \"Smoking hot React Notifications. Lightweight, customizable and beautifu"
},
{
"path": "site/.gitignore",
"chars": 386,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "site/README.md",
"chars": 1206,
"preview": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js"
},
{
"path": "site/components/code.tsx",
"chars": 2781,
"preview": "import clsx from 'clsx';\nimport Highlight, {\n defaultProps,\n Language,\n PrismTheme,\n} from 'prism-react-renderer';\n\nc"
},
{
"path": "site/components/docs-layout.tsx",
"chars": 2991,
"preview": "import * as React from 'react';\nimport { Toaster } from 'react-hot-toast';\nimport { NextSeo } from 'next-seo';\nimport Li"
},
{
"path": "site/components/emoji-button.tsx",
"chars": 548,
"preview": "export const EmojiButton: React.FC<{\n onClick: () => void;\n emoji: string | React.ReactElement;\n children?: React.Rea"
},
{
"path": "site/components/sections/footer.tsx",
"chars": 1369,
"preview": "import React from 'react';\nimport Link from 'next/link';\n\nexport function Footer({ noBadge }: { noBadge?: boolean }) {\n "
},
{
"path": "site/components/sections/splitbee-counter.tsx",
"chars": 1932,
"preview": "import React from 'react';\nimport clsx from 'clsx';\n\nexport const useSplitbeeCount = <T extends string>(\n event: T,\n t"
},
{
"path": "site/components/sections/toast-example.tsx",
"chars": 7897,
"preview": "import React, { useState } from 'react';\nimport toast from 'react-hot-toast';\n\nimport { EmojiButton } from '../emoji-but"
},
{
"path": "site/components/sections/toaster-example.tsx",
"chars": 3064,
"preview": "import clsx from 'clsx';\nimport toast, { ToastPosition } from 'react-hot-toast';\nimport Arrow from '../../assets/arrow.s"
},
{
"path": "site/next-env.d.ts",
"chars": 201,
"preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edite"
},
{
"path": "site/next.config.mjs",
"chars": 820,
"preview": "import rehypeSlug from 'rehype-slug';\nimport remarkGfm from 'remark-gfm';\nimport nextMdx from '@next/mdx';\n\nconst withMD"
},
{
"path": "site/package.json",
"chars": 870,
"preview": "{\n \"name\": \"site\",\n \"scripts\": {\n \"dev\": \"next dev\",\n \"build\": \"next build\",\n \"start\": \"next start\"\n },\n \"d"
},
{
"path": "site/pages/_app.tsx",
"chars": 2041,
"preview": "import '../styles/tailwind-utils.css';\nimport '../styles/main.css';\nimport * as React from 'react';\nimport Link from 'ne"
},
{
"path": "site/pages/docs/index.mdx",
"chars": 735,
"preview": "import Layout from '../../components/docs-layout';\nimport toast from 'react-hot-toast';\n\nexport const meta = {\n title: "
},
{
"path": "site/pages/docs/multi-toaster.mdx",
"chars": 3062,
"preview": "import Layout from '../../components/docs-layout';\nimport toast, { Toaster } from 'react-hot-toast';\n\nexport const meta "
},
{
"path": "site/pages/docs/styling.mdx",
"chars": 2284,
"preview": "import Layout from '../../components/docs-layout';\nimport toast from 'react-hot-toast';\n\nexport const meta = {\n title: "
},
{
"path": "site/pages/docs/toast-bar.mdx",
"chars": 1253,
"preview": "import Layout from '../../components/docs-layout';\nimport toast from 'react-hot-toast';\n\nexport const meta = {\n title: "
},
{
"path": "site/pages/docs/toast.mdx",
"chars": 5711,
"preview": "import Layout from '../../components/docs-layout';\nimport toast from 'react-hot-toast';\n\nexport const meta = {\n title: "
},
{
"path": "site/pages/docs/toaster.mdx",
"chars": 3298,
"preview": "import Layout from '../../components/docs-layout';\nimport toast from 'react-hot-toast';\n\nexport const meta = {\n title: "
},
{
"path": "site/pages/docs/use-toaster-store.mdx",
"chars": 663,
"preview": "import Layout from '../../components/docs-layout';\nimport toast from 'react-hot-toast';\n\nexport const meta = {\n title: "
},
{
"path": "site/pages/docs/use-toaster.mdx",
"chars": 4946,
"preview": "import Layout from '../../components/docs-layout';\nimport toast from 'react-hot-toast';\n\nexport const meta = {\n title: "
},
{
"path": "site/pages/docs/version-2.mdx",
"chars": 7210,
"preview": "import Layout from '../../components/docs-layout';\nimport toast from 'react-hot-toast';\n\nexport const meta = {\n title: "
},
{
"path": "site/pages/index.tsx",
"chars": 8448,
"preview": "import { NextSeo } from 'next-seo';\nimport toast, {\n Toaster,\n useToasterStore,\n ToastPosition,\n} from 'react-hot-toa"
},
{
"path": "site/postcss.config.js",
"chars": 50,
"preview": "module.exports = {\n plugins: ['tailwindcss'],\n};\n"
},
{
"path": "site/styles/main.css",
"chars": 293,
"preview": "@tailwind base;\n@tailwind components;\n\nhtml,\nbody,\nbody > div {\n @apply flex flex-col justify-between flex-1 min-h-full"
},
{
"path": "site/styles/prism-theme.css",
"chars": 2726,
"preview": "/* Generated with http://k88hudson.github.io/syntax-highlighting-theme-generator/www */\n/* http://k88hudson.github.io/re"
},
{
"path": "site/styles/tailwind-utils.css",
"chars": 21,
"preview": "@tailwind utilities;\n"
},
{
"path": "site/tailwind.config.js",
"chars": 4009,
"preview": "module.exports = {\n mode: 'jit',\n content: [\n './pages/*.tsx',\n './pages/**/*.tsx',\n './pages/*.mdx',\n './"
},
{
"path": "site/tsconfig.json",
"chars": 652,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"es5\",\n \"lib\": [\n \"dom\",\n \"dom.iterable\",\n \"esnext\"\n ],\n "
},
{
"path": "site/types/mdx.d.ts",
"chars": 114,
"preview": "declare module '*.mdx' {\n let MDXComponent: (props: any) => React.ReactElement;\n export default MDXComponent;\n}\n"
},
{
"path": "site/types/svg.d.ts",
"chars": 177,
"preview": "interface SvgrComponent\n extends React.StatelessComponent<React.SVGAttributes<SVGElement>> {}\n\ndeclare module '*.svg' {"
},
{
"path": "src/components/checkmark.tsx",
"chars": 1161,
"preview": "import { styled, keyframes } from 'goober';\n\nconst circleAnimation = keyframes`\nfrom {\n transform: scale(0) rotate(45de"
},
{
"path": "src/components/error.tsx",
"chars": 1356,
"preview": "import { styled, keyframes } from 'goober';\n\nconst circleAnimation = keyframes`\nfrom {\n transform: scale(0) rotate(45de"
},
{
"path": "src/components/loader.tsx",
"chars": 544,
"preview": "import { styled, keyframes } from 'goober';\n\nconst rotate = keyframes`\n from {\n transform: rotate(0deg);\n }\n to {\n"
},
{
"path": "src/components/toast-bar.tsx",
"chars": 2957,
"preview": "import * as React from 'react';\nimport { styled, keyframes } from 'goober';\n\nimport { Toast, ToastPosition, resolveValue"
},
{
"path": "src/components/toast-icon.tsx",
"chars": 1644,
"preview": "import * as React from 'react';\nimport { styled, keyframes } from 'goober';\n\nimport { Toast } from '../core/types';\nimpo"
},
{
"path": "src/components/toaster.tsx",
"chars": 3391,
"preview": "import { css, setup } from 'goober';\nimport * as React from 'react';\nimport {\n resolveValue,\n ToasterProps,\n ToastPos"
},
{
"path": "src/core/store.ts",
"chars": 5253,
"preview": "import { useEffect, useState, useRef } from 'react';\nimport { DefaultToastOptions, Toast, ToastType } from './types';\n\ne"
},
{
"path": "src/core/toast.ts",
"chars": 3505,
"preview": "import {\n Renderable,\n Toast,\n ToastOptions,\n ToastType,\n DefaultToastOptions,\n ValueOrFunction,\n resolveValue,\n}"
},
{
"path": "src/core/types.ts",
"chars": 2204,
"preview": "import { CSSProperties } from 'react';\n\nexport type ToastType = 'success' | 'error' | 'loading' | 'blank' | 'custom';\nex"
},
{
"path": "src/core/use-toaster.ts",
"chars": 3724,
"preview": "import { useEffect, useCallback, useRef } from 'react';\nimport { createDispatch, ActionType, useStore, dispatch } from '"
},
{
"path": "src/core/utils.ts",
"chars": 509,
"preview": "export const genId = (() => {\n let count = 0;\n return () => {\n return (++count).toString();\n };\n})();\n\nexport cons"
},
{
"path": "src/headless/index.ts",
"chars": 434,
"preview": "import { toast } from '../core/toast';\n\nexport type {\n DefaultToastOptions,\n IconTheme,\n Renderable,\n Toast,\n Toast"
},
{
"path": "src/index.ts",
"chars": 415,
"preview": "import { toast } from './core/toast';\n\nexport * from './headless';\n\nexport { ToastBar } from './components/toast-bar';\ne"
},
{
"path": "test/setup.ts",
"chars": 667,
"preview": "import '@testing-library/jest-dom';\n\n// Mock matchMedia\nObject.defineProperty(window, 'matchMedia', {\n writable: true,\n"
},
{
"path": "test/toast.test.tsx",
"chars": 12835,
"preview": "import React, { useEffect, useState } from 'react';\nimport {\n render,\n screen,\n act,\n waitFor,\n fireEvent,\n} from '"
},
{
"path": "tsconfig.json",
"chars": 1401,
"preview": "{\n // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs\n \"include\": [\"src\", \"types\", \"test\"],"
},
{
"path": "tsup.config.ts",
"chars": 705,
"preview": "import { defineConfig, Options } from 'tsup';\nimport { minifyTemplates, writeFiles } from 'esbuild-minify-templates';\n\nc"
}
]
About this extraction
This page contains the full source code of the timolins/react-hot-toast GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 55 files (118.4 KB), approximately 32.7k tokens, and a symbol index with 36 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.