FUNSTACK UI ships no visual CSS, but it does use inline styles for placement and two tiny functional stylesheets. Under a style-src that lists nonces or hashes instead of 'unsafe-inline', here is what each needs. Everything is exported from @funstack/ui/csp (and the root entry).
| Mechanism | Example | Under a strict style-src |
|---|---|---|
Inline style set on the client | the point of a context menu, position-area after an update | Allowed: React writes the style prop through the CSSOM, which CSP does not govern. |
Inline style in server-rendered HTML | position-area of an anchored popup, Tooltip's interest-delay-* | Blocked without style-src-attr 'unsafe-inline' (as for every library that renders inline styles). Blocked, the popup is placed by your stylesheet only until React updates the attribute, and a custom tooltip delay falls back to the browser's. |
Hoisted <style> elements | the two stylesheets below | Blocked unless the text matches a 'sha256-…' source or the element carries your nonce. |
Each is a constant string rendered as a React hoisted <style> (one per page, deduplicated by its href), only by the part that needs it. Their base64 SHA-256 is exported as STYLE_SHA256, keyed by href; a test recomputes it from the CSS, so it cannot drift from what the parts render.
| `href` | Rendered by | When | `style-src` source |
|---|---|---|---|
@funstack/ui/menu-point | Menu.Popup | On the client, the first time a menu opens at a point (Menu.ContextTrigger) without side={null} | 'sha256-Wm5aBRstuZ3S9kDs0IbldhidU/prqpIW69jEAS0N9mc=' |
@funstack/ui/tooltip-warm | Tooltip.Trigger | With experimental_interestFor only, during server rendering too | 'sha256-nFBZGhNvo46ITrlBb48hELUAyWzpeIdJCziCectFQa4=' |
Content-Security-Policy: style-src 'self' 'sha256-Wm5aBRstuZ3S9kDs0IbldhidU/prqpIW69jEAS0N9mc=' 'sha256-nFBZGhNvo46ITrlBb48hELUAyWzpeIdJCziCectFQa4='; style-src-attr 'unsafe-inline'import { STYLE_SHA256 } from "@funstack/ui/csp";
const styleSources = Object.values(STYLE_SHA256).map((hash) => `'sha256-${hash}'`);Hashes need no code and suit static sites (FUNSTACK Static's output, a CDN): the values only change with a library release, so compute the header from STYLE_SHA256 at build time rather than copying them.
With a per-response nonce, render <CSPProvider nonce> once around the app. Every part that renders a hoisted <style> puts the nonce on it. It is the library's only provider, and its prop is serialisable, so a Server Component layout that knows the nonce renders it directly.
import { CSPProvider } from "@funstack/ui/csp";
export function Layout({ nonce, children }: { nonce: string; children: React.ReactNode }) {
return <CSPProvider nonce={nonce}>{children}</CSPProvider>;
}
// Server: pass the same value as React's style nonce.
renderToReadableStream(<App nonce={nonce} />, { nonce: { script: nonce, style: nonce } });| Prop | Type | Default | Description |
|---|---|---|---|
nonce | string | – | The nonce of your style-src, forwarded as the nonce of every library <style>. Use the value you pass as React's nonce.style. |
children | ReactNode | – | Your app. |
The library does not read a <meta property="csp-nonce"> on its own: the server render needs the value in props anyway.
delay instead of opening at once (the Trigger's var(--fsui-tooltip-warm, 0) falls back to 0, not to the browser's own delay). The JS path does not use the stylesheet.