FUNSTACK UI

HomeGetting startedConceptsComponentsRoadmap
GitHub

FUNSTACK UI is experimental. Part of the FUNSTACK family with FUNSTACK Router and FUNSTACK Static, which build this site.

Introduction

  • Overview
  • Getting started

Concepts

  • Action props
  • Pending & optimistic state
  • Transitions & ViewTransition
  • Suspense
  • Server Components
  • Browser-owned state
  • Experimental features
  • Content Security Policy

Components

  • Button
  • Toggle
  • ToggleGroup
  • Tabs
  • Accordion
  • Tooltip
  • Dialog
  • Popover
  • Menu
  • Context menu
  • Toolbar
  • Switch
  • Checkbox
  • RadioGroup
  • CheckboxGroup
  • Select
  • Combobox
  • Toast
  • Slider
  • NumberField
  • Field & Form
  • Progress
  • Meter

Resources

  • Roadmap & changelog
  • GitHub ↗

Content Security Policy

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).

What the library puts in the page

MechanismExampleUnder a strict style-src
Inline style set on the clientthe point of a context menu, position-area after an updateAllowed: React writes the style prop through the CSSOM, which CSP does not govern.
Inline style in server-rendered HTMLposition-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> elementsthe two stylesheets belowBlocked unless the text matches a 'sha256-…' source or the element carries your nonce.

The shipped stylesheets and their hashes

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 byWhen`style-src` source
@funstack/ui/menu-pointMenu.PopupOn the client, the first time a menu opens at a point (Menu.ContextTrigger) without side={null}'sha256-Wm5aBRstuZ3S9kDs0IbldhidU/prqpIW69jEAS0N9mc='
@funstack/ui/tooltip-warmTooltip.TriggerWith 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.

Nonces: CSPProvider

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 } });

Pass the same value as React's nonce.style

During server rendering React (19.3) checks a hoisted <style> against the nonce: { style } render option:

  • Same value on the element and the option: written with the nonce.
  • If you pass nonce.style, render <CSPProvider> with the same value; without it React drops the rule (the element has no nonce), and so does a different value.
  • No nonce option, or the string form nonce: "…" (scripts only): the rule is written without a nonce, so a nonce policy blocks it in the browser.

This matters for the tooltip stylesheet, which can render on the server; the menu stylesheet is only ever inserted on the client.

PropTypeDefaultDescription
noncestring–The nonce of your style-src, forwarded as the nonce of every library <style>. Use the value you pass as React's nonce.style.
childrenReactNode–Your app.

The library does not read a <meta property="csp-nonce"> on its own: the server render needs the value in props anyway.

If a stylesheet is blocked

  • Menu point rules: a context menu still opens at the pointer, but it never flips at the viewport edges.
  • Tooltip skip-delay rule: on Chromium's native path the next tooltip waits its 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.