Content Security Policy

A Content Security Policy restricts where scripts and styles may come from. The strictest policies forbid 'unsafe-inline' and 'unsafe-eval' entirely. LemonadeJS runs under such a policy with no configuration: no nonce, no global, no opt-in.

The short version:

  • script-src needs no exceptions. The engine never calls eval or new Function, and never writes inline on* handler attributes.
  • style-src needs no exceptions. Bound style="…" values and component-owned <style> are both applied through the CSSOM, which CSP does not govern.

So a policy as tight as default-src 'self' runs the engine, your components, and Studio blocks unchanged.

Scripts

Nothing requires 'unsafe-eval': there is no eval, no new Function, no string-compiled templates. Tagged templates are parsed into a node structure in plain JavaScript.

Nothing requires 'unsafe-inline' for scripts either. Events you write in a template, like this:

html`<button onclick="${() => count.value++}">+</button>`

are not rendered as onclick="" attributes. The engine removes the placeholder and binds the handler with addEventListener. The DOM a CSP scanner sees has no inline script.

Styles

The design choice matters here. CSP’s style-src governs markup: a parsed style attribute (style-src-attr) and <style> / <link> elements (style-src-elem). It does not govern the CSSOM: styles applied through JavaScript DOM APIs are trusted. LemonadeJS routes both kinds of styling through the CSSOM.

Bound styles

A bound style value is applied with element.style, never setAttribute('style', …):

// what you write
html`<div style="${() => css({ width: w.value })}">`

// what the engine does (CSSOM, not the style attribute)
el.style.cssText = 'width: 240px';

style-src-attr blocks parsing a style attribute; the CSSOM write sidesteps it. (One observable consequence: reading element.getAttribute('style') back returns the browser-normalized serialization (width: 240px;), not the exact string you wrote. Assert against the CSSOM, e.g. element.style.width, not the raw attribute.)

Component-owned <style>

A component can ship its CSS in its own source with a <style> tag inside html “ (see Styling). The engine lifts that CSS and injects it once per template, as a constructed stylesheet adopted via document.adoptedStyleSheets:

const sheet = new CSSStyleSheet();
sheet.replaceSync(componentCss);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];

A constructed stylesheet is a CSSOM object, not a <style> element, so style-src-elem does not apply. No nonce, no 'unsafe-inline'. adoptedStyleSheets is available in every evergreen browser since 2023.

Images

Blocks that export a canvas to a data URL, the cropper and color picker, produce data: image sources. If you display them, allow it:

img-src 'self' data:;

A strict policy that works

Content-Security-Policy:
    default-src 'self';
    script-src 'self';
    style-src 'self';
    img-src 'self' data:;

No 'unsafe-inline', no 'unsafe-eval', no 'nonce-…' required for the framework. Add directives for your own assets as usual.

The one edge case

A browser old enough to lack constructable stylesheets (pre-2023) falls back to a real <style> element for component CSS, and that element would be blocked by a strict style-src. If you must support such browsers under a strict policy, either add 'unsafe-inline' to style-src or ship the affected components’ CSS as a static <link rel="stylesheet"> from 'self'. On any current browser there is nothing to configure.

Verifying it yourself

The behavior is guarded by a real-browser test: a page served with style-src 'self' (no 'unsafe-inline', no nonce) mounts a component and asserts that a bound style value and a component <style> both take effect, and that the component CSS arrived via document.adoptedStyleSheets. If a change reintroduced setAttribute('style') or a raw <style> element on the primary path, that check would fail.