diff --git a/docs/setup.md b/docs/setup.md index 671b8b611..43011f86a 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -118,6 +118,48 @@ jss.setup({ }) ``` +## Using a `CSSStyleSheet` instance for secure style injection + +For environments with strict Content Security Policy (CSP) settings, JSS now supports injecting styles into a `CSSStyleSheet` instance. This approach is particularly useful when inline styles are restricted and a nonce value is unavailable or not exposed. + +### Example + +Create a `CSSStyleSheet` instance and pass it to JSS during setup: + +```javascript +import jss from 'jss' + +const sheet = new CSSStyleSheet() +document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet] + +jss.setup({ + insertionPoint: sheet // Pass the CSSStyleSheet instance +}) + +// Create your style. +const style = { + myButton: { + color: 'green' + } +} + +// Compile styles, apply plugins. +const jssSheet = jss.createStyleSheet(style) + +// Inject styles directly into the provided CSSStyleSheet. +jssSheet.attach() +``` + +### Benefits + +- Enables secure style injection into a `CSSStyleSheet` instance, avoiding inline styles. +- Works seamlessly in CSP-enabled applications where the nonce attribute cannot be used or accessed. + +### Notes + +- This feature is an alternative to nonce-based CSP compliance. Both approaches are supported. +- Ensure the provided `CSSStyleSheet` instance is valid and adopted by the document where styles need to be applied. + ## Configuring Content Security Policy You might need to set the `style-src` CSP directive, but do not want to set it to `unsafe-inline`. See [these instructions for configuring CSP](csp.md). diff --git a/packages/jss/src/DomRenderer.js b/packages/jss/src/DomRenderer.js index 5151ca7f3..ec5a738db 100644 --- a/packages/jss/src/DomRenderer.js +++ b/packages/jss/src/DomRenderer.js @@ -210,6 +210,16 @@ function insertStyle(style, options) { return } + // if insertionPoint is instance of CSSStyleSheet then we can insert the rule at the end of the stylesheet + // and we don't need to create a new style element + if (insertionPoint instanceof CSSStyleSheet) { + try { + insertionPoint.insertRule(style.textContent, insertionPoint.cssRules.length) + } catch (err) { + warning(false, `[JSS] ${err.message}`) + } + return + } getHead().appendChild(style) } @@ -276,13 +286,19 @@ export default class DomRenderer { if (sheet) sheets.add(sheet) this.sheet = sheet - const {media, meta, element} = this.sheet ? this.sheet.options : {} - this.element = element || createStyle() - this.element.setAttribute('data-jss', '') - if (media) this.element.setAttribute('media', media) - if (meta) this.element.setAttribute('data-meta', meta) - const nonce = getNonce() - if (nonce) this.element.setAttribute('nonce', nonce) + const {media, meta, element, insertionPoint} = this.sheet ? this.sheet.options : {} + if (insertionPoint instanceof CSSStyleSheet) { + // If insertionPoint is an instance of CSSStyleSheet, use it directly. + this.element = insertionPoint + } else { + // Otherwise, create a new style element. + this.element = element || createStyle() + this.element.setAttribute('data-jss', '') + if (media) this.element.setAttribute('media', media) + if (meta) this.element.setAttribute('data-meta', meta) + const nonce = getNonce() + if (nonce) this.element.setAttribute('nonce', nonce) + } } /** @@ -345,14 +361,21 @@ export default class DomRenderer { * Insert a rule into element. */ insertRule(rule, index, nativeParent = this.element.sheet) { + // create a new variable to hold the nativeParent + let parentNode = nativeParent + // If the element is an instance of CSSStyleSheet, use it directly. + if (this.element instanceof CSSStyleSheet) { + parentNode = this.element + } + if (rule.rules) { const parent = rule - let latestNativeParent = nativeParent + let latestNativeParent = parentNode if (rule.type === 'conditional' || rule.type === 'keyframes') { - const insertionIndex = getValidRuleInsertionIndex(nativeParent, index) + const insertionIndex = getValidRuleInsertionIndex(parentNode, index) // We need to render the container without children first. latestNativeParent = insertRule( - nativeParent, + parentNode, parent.toString({children: false}), insertionIndex ) @@ -369,8 +392,8 @@ export default class DomRenderer { if (!ruleStr) return false - const insertionIndex = getValidRuleInsertionIndex(nativeParent, index) - const nativeRule = insertRule(nativeParent, ruleStr, insertionIndex) + const insertionIndex = getValidRuleInsertionIndex(parentNode, index) + const nativeRule = insertRule(parentNode, ruleStr, insertionIndex) if (nativeRule === false) { return false }