Dialog/DropdownMenu scroll-lock sets inline styles, breaks CSP with hashed style-src
under review
# Dialog/DropdownMenu scroll-lock sets inline styles, breaks CSP with hashed style-src
## Description
bits-ui's scroll-lock utility sets inline styles at runtime (`style.overflow`, `style.paddingRight`, `style.pointerEvents`, `style.userSelect`, `style.webkitUserSelect`, `style.marginRight`) on `document.body` and `document.documentElement`. This breaks Content Security Policy when `style-src` uses hash-based directives instead of `unsafe-inline`.
Modern CSP configurations (and frameworks like Astro 6 that auto-hash styles) reject inline style mutations when hashes are present in the `style-src` directive. This makes bits-ui Dialog and DropdownMenu incompatible with strict CSP out of the box.
## Environment
- **bits-ui**: v2.16.3
- **Svelte**: 5.55.0
- **Framework**: Astro 6 with `security.csp` enabled (SHA-256 hashing)
- **Adapter**: @astrojs/cloudflare
## Reproduction
1. Enable CSP in Astro config:
```ts
security: {
csp: {
algorithm: "SHA-256",
scriptDirective: { resources: ["'self'"] },
},
},
```
2. Use `Dialog.Content` (even with `preventScroll={false}`):
```svelte
<Dialog.Root>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content preventScroll={false}>
Hello
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
```
3. Build and preview. Browser console shows:
```
Applying inline style violates the following Content Security Policy directive
'style-src 'self' 'sha256-...' 'sha256-...''
```
## Root Cause
The scroll-lock module (`body-scroll-lock.svelte.js`) sets these inline styles:
```js
document.body.style.paddingRight = `${config.padding}px`;
document.body.style.marginRight = `${config.margin}px`;
document.body.style.setProperty("--scrollbar-width", `${verticalScrollbarWidth}px`);
document.body.style.overflow = "hidden";
document.body.style.pointerEvents = "none";
```
These runtime inline style mutations cannot be pre-hashed by CSP.
## Issues
### 1. `preventScroll={false}` doesn't prevent the scroll-lock module from being bundled
Even when `preventScroll={false}` is set on `Dialog.Content`, the scroll-lock code is still included in the production bundle. While it may not execute on dialog open, it appears to execute during initialization or internal state changes, triggering the CSP violation.
### 2. `DropdownMenu.Content` doesn't expose `preventScroll` at all
`Dialog.Content` exposes `preventScroll` as a prop, but `DropdownMenu.Content` (which inherits from `PopperLayer`) deliberately omits it from the type definition. There's no way for consumers to opt out of scroll-lock on dropdown menus.
## Suggested Fixes
1. **Tree-shake scroll-lock when `preventScroll={false}`** — The scroll-lock module should not be bundled at all when the prop is statically `false`.
2. **Expose `preventScroll` on `DropdownMenu.Content`** — Allow consumers to disable scroll-lock on dropdown menus, same as `Dialog.Content`.
3. **Use CSS classes instead of inline styles for scroll-lock** — Instead of `document.body.style.overflow = "hidden"`, apply a CSS class like `document.body.classList.add("bits-scroll-locked")` and ship a corresponding CSS rule. This is CSP-compatible because class toggles don't violate `style-src` directives.
## Workaround
We replaced bits-ui Dialog with a custom Svelte 5 implementation that uses `document.documentElement.classList.toggle("overflow-hidden", open)` (a Tailwind utility class) for scroll-lock. This is fully CSP-compatible but means giving up bits-ui's Dialog primitives.
1 条评论