Skip to content

Commit 73218ae

Browse files
committed
feat: Added strict CSP mode that uses CSS-only
1 parent f339d71 commit 73218ae

File tree

5 files changed

+287
-36
lines changed

5 files changed

+287
-36
lines changed

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,48 @@ const App = () => {
6767
};
6868
```
6969

70+
## Content Security Policy (CSP)
71+
72+
react-hot-toast supports strict Content Security Policies through an opt-in strict CSP mode.
73+
74+
### Default Mode
75+
76+
By default, react-hot-toast uses inline styles for maximum flexibility. This requires `style-src 'unsafe-inline'` in your CSP.
77+
78+
### Strict CSP Mode
79+
80+
For applications with strict CSP that disallow inline styles, enable strict CSP mode:
81+
82+
```jsx
83+
import toast, { Toaster } from 'react-hot-toast';
84+
85+
<Toaster strictCSP={true} />
86+
```
87+
88+
In strict CSP mode:
89+
- All inline `style` props are ignored
90+
- Styling must be done via CSS classes and CSS variables
91+
- Toast positioning uses CSS flexbox instead of inline transforms
92+
- Fully compatible with CSP `style-src 'nonce-...'` directives
93+
94+
The library uses [goober](https://github.com/cristianbote/goober) for styling, which supports CSP nonces through multiple methods:
95+
96+
**Using `setNonce()`:**
97+
```js
98+
import { setNonce } from 'goober';
99+
setNonce('your-nonce-here');
100+
```
101+
102+
**Vite convention:**
103+
```html
104+
<meta property="csp-nonce" nonce="your-nonce-here" />
105+
```
106+
107+
**Webpack convention:**
108+
Set `window.__webpack_nonce__` in your entry script (see [webpack CSP guide](https://webpack.js.org/guides/csp/)).
109+
110+
Make sure your CSP includes `style-src 'nonce-your-nonce-here'` and `script-src 'nonce-your-nonce-here'`.
111+
70112
## Documentation
71113

72114
Find the full API reference on [official documentation](https://react-hot-toast.com/docs).

site/pages/docs/styling.mdx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,41 @@ import { Toaster, ToastBar } from 'react-hot-toast';
128128
)}
129129
</Toaster>;
130130
```
131+
132+
## Strict CSP Mode
133+
134+
For applications with strict Content Security Policies that disallow inline styles, enable `strictCSP` mode:
135+
136+
```jsx
137+
<Toaster strictCSP={true} />
138+
```
139+
140+
In strict CSP mode:
141+
- All inline `style` props are ignored
142+
- Styling must be done via CSS classes and CSS variables
143+
- Toast positioning uses CSS flexbox instead of inline transforms
144+
- The library is fully compatible with CSP `style-src 'nonce-...'` directives
145+
146+
### Styling with CSS Variables (Strict CSP)
147+
148+
When using strict CSP mode, use CSS variables for theming:
149+
150+
```css
151+
:root {
152+
/* Success toasts */
153+
--rht-success-bg: #ecfdf5;
154+
--rht-success-fg: #065f46;
155+
156+
/* Error toasts */
157+
--rht-error-bg: #fef2f2;
158+
--rht-error-fg: #991b1b;
159+
160+
/* Loading toasts */
161+
--rht-loading-bg: #eff6ff;
162+
--rht-loading-fg: #1e40af;
163+
164+
/* Blank toasts */
165+
--rht-blank-bg: #fff;
166+
--rht-blank-fg: #363636;
167+
}
168+
```

src/components/toast-bar.tsx

Lines changed: 110 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,10 @@
11
import * as React from 'react';
2-
import { styled, keyframes } from 'goober';
2+
import { styled } from 'goober';
33

44
import { Toast, ToastPosition, resolveValue, Renderable } from '../core/types';
55
import { ToastIcon } from './toast-icon';
66
import { prefersReducedMotion } from '../core/utils';
77

8-
const enterAnimation = (factor: number) => `
9-
0% {transform: translate3d(0,${factor * -200}%,0) scale(.6); opacity:.5;}
10-
100% {transform: translate3d(0,0,0) scale(1); opacity:1;}
11-
`;
12-
13-
const exitAnimation = (factor: number) => `
14-
0% {transform: translate3d(0,0,-1px) scale(1); opacity:1;}
15-
100% {transform: translate3d(0,${factor * -150}%,-1px) scale(.6); opacity:0;}
16-
`;
17-
18-
const fadeInAnimation = `0%{opacity:0;} 100%{opacity:1;}`;
19-
const fadeOutAnimation = `0%{opacity:1;} 100%{opacity:0;}`;
20-
218
// Use :where() for zero specificity - allows Tailwind to override easily
229
const ToastBarBase = styled('div')`
2310
:where(&) {
@@ -33,6 +20,84 @@ const ToastBarBase = styled('div')`
3320
padding: 8px 10px;
3421
border-radius: 8px;
3522
}
23+
24+
@keyframes rht-enter-from-top {
25+
0% { transform: translate3d(0, -200%, 0) scale(.6); opacity: .5; }
26+
100% { transform: translate3d(0, 0, 0) scale(1); opacity: 1; }
27+
}
28+
29+
@keyframes rht-enter-from-bottom {
30+
0% { transform: translate3d(0, 200%, 0) scale(.6); opacity: .5; }
31+
100% { transform: translate3d(0, 0, 0) scale(1); opacity: 1; }
32+
}
33+
34+
@keyframes rht-exit-to-top {
35+
0% { transform: translate3d(0, 0, -1px) scale(1); opacity: 1; }
36+
100% { transform: translate3d(0, -150%, -1px) scale(.6); opacity: 0; }
37+
}
38+
39+
@keyframes rht-exit-to-bottom {
40+
0% { transform: translate3d(0, 0, -1px) scale(1); opacity: 1; }
41+
100% { transform: translate3d(0, 150%, -1px) scale(.6); opacity: 0; }
42+
}
43+
44+
@keyframes rht-fade-in {
45+
0% { opacity: 0; }
46+
100% { opacity: 1; }
47+
}
48+
49+
@keyframes rht-fade-out {
50+
0% { opacity: 1; }
51+
100% { opacity: 0; }
52+
}
53+
54+
&.rht-enter-from-top {
55+
animation: rht-enter-from-top 0.35s cubic-bezier(.21,1.02,.73,1) forwards;
56+
}
57+
58+
&.rht-enter-from-bottom {
59+
animation: rht-enter-from-bottom 0.35s cubic-bezier(.21,1.02,.73,1) forwards;
60+
}
61+
62+
&.rht-exit-to-top {
63+
animation: rht-exit-to-top 0.4s forwards cubic-bezier(.06,.71,.55,1);
64+
}
65+
66+
&.rht-exit-to-bottom {
67+
animation: rht-exit-to-bottom 0.4s forwards cubic-bezier(.06,.71,.55,1);
68+
}
69+
70+
&.rht-fade-in {
71+
animation: rht-fade-in 0.35s cubic-bezier(.21,1.02,.73,1) forwards;
72+
}
73+
74+
&.rht-fade-out {
75+
animation: rht-fade-out 0.4s forwards cubic-bezier(.06,.71,.55,1);
76+
}
77+
78+
&.rht-invisible {
79+
opacity: 0;
80+
}
81+
82+
&.rht-success {
83+
background: var(--rht-success-bg, #ecfdf5);
84+
color: var(--rht-success-fg, #065f46);
85+
}
86+
87+
&.rht-error {
88+
background: var(--rht-error-bg, #fef2f2);
89+
color: var(--rht-error-fg, #991b1b);
90+
}
91+
92+
&.rht-loading {
93+
background: var(--rht-loading-bg, #fff);
94+
color: var(--rht-loading-fg, #363636);
95+
}
96+
97+
&.rht-blank {
98+
background: var(--rht-blank-bg, #fff);
99+
color: var(--rht-blank-fg, #363636);
100+
}
36101
`;
37102

38103
const Message = styled('div')`
@@ -54,32 +119,36 @@ interface ToastBarProps {
54119
}) => Renderable;
55120
}
56121

57-
const getAnimationStyle = (
122+
const getAnimationClass = (
58123
position: ToastPosition,
59-
visible: boolean
60-
): React.CSSProperties => {
124+
visible: boolean,
125+
hasHeight: boolean
126+
): string => {
127+
if (!hasHeight) {
128+
return 'rht-invisible';
129+
}
130+
61131
const top = position.includes('top');
62-
const factor = top ? 1 : -1;
132+
const reduced = prefersReducedMotion();
63133

64-
const [enter, exit] = prefersReducedMotion()
65-
? [fadeInAnimation, fadeOutAnimation]
66-
: [enterAnimation(factor), exitAnimation(factor)];
134+
if (reduced) {
135+
return visible ? 'rht-fade-in' : 'rht-fade-out';
136+
}
137+
138+
if (visible) {
139+
return top ? 'rht-enter-from-top' : 'rht-enter-from-bottom';
140+
}
67141

68-
return {
69-
animation: visible
70-
? `${keyframes(enter)} 0.35s cubic-bezier(.21,1.02,.73,1) forwards`
71-
: `${keyframes(exit)} 0.4s forwards cubic-bezier(.06,.71,.55,1)`,
72-
};
142+
return top ? 'rht-exit-to-top' : 'rht-exit-to-bottom';
73143
};
74144

75145
export const ToastBar: React.FC<ToastBarProps> = React.memo(
76146
({ toast, position, style, children }) => {
77-
const animationStyle: React.CSSProperties = toast.height
78-
? getAnimationStyle(
79-
toast.position || position || 'top-center',
80-
toast.visible
81-
)
82-
: { opacity: 0 };
147+
const animationClass = getAnimationClass(
148+
toast.position || position || 'top-center',
149+
toast.visible,
150+
!!toast.height
151+
);
83152

84153
const icon = <ToastIcon toast={toast} />;
85154
const message = (
@@ -88,11 +157,18 @@ export const ToastBar: React.FC<ToastBarProps> = React.memo(
88157
</Message>
89158
);
90159

160+
const className = [
161+
toast.className,
162+
animationClass,
163+
toast.type ? `rht-${toast.type}` : null,
164+
]
165+
.filter(Boolean)
166+
.join(' ');
167+
91168
return (
92169
<ToastBarBase
93-
className={toast.className}
170+
className={className}
94171
style={{
95-
...animationStyle,
96172
...style,
97173
...toast.style,
98174
}}

0 commit comments

Comments
 (0)