Skip to content

feat!: Allow using Blockly in web components/shadow DOM#9611

Merged
gonfunko merged 9 commits into
v13from
shadow-dom
Mar 6, 2026
Merged

feat!: Allow using Blockly in web components/shadow DOM#9611
gonfunko merged 9 commits into
v13from
shadow-dom

Conversation

@gonfunko
Copy link
Copy Markdown
Contributor

@gonfunko gonfunko commented Mar 3, 2026

The basics

The details

Resolves

Fixes #1114

Proposed Changes

This PR adds support for using Blockly inside of the shadow DOM and web components.

Largely, this is a matter of injecting CSS into the document or shadow DOM root as appropriate, rather than creating <style> tags and injecting them into <head>. Additionally, the global floaty elements (tooltips, input fields, widget div, dropdown div) needed some adjustments to their positioning logic; they had generally all been assuming that they lived in a global div at the root of the document, but this was already not necessarily the case. We've allowed specifying a parent element for these, but if that had bounds smaller than the document things could get wonky. Now, the parent container defaults to returning the injection div unless another value is specified, so these elements are all contained within that (and the shadow DOM/web component, if Blockly is used in one), and that element's position in the page is taken into account when determining the bounds for tooltips, dropdowns, etc.

I also added an additional playground/test harness (with LLM assistance) that embeds Blockly inside a <div> as usual, but also defines a web component and uses that to embed a separate instance on the same page.

Reason for Changes

This has been a long-standing request, and web components have become more widespread and popular. We also have some partner applications that could benefit from this.

Breaking Changes

  • getParentContainer() returns the injection div if another element has not been specified. This behavior is likely fine, but if you were using this method you should either explicitly set the container you want or verify that your use is compatible with the injection div.
  • Blockly.Css.inject() requires that the element into which Blockly/the CSS should be injected be specified.
  • The tagName argument has been removed from ConstantProvider.createDom() and ConstantProvider.injectCss_(), since CSS is no longer added via a <style> tag. If you have a custom renderer or ConstantProvider or call these methods, ensure that the tagName argument is removed from your implementation/calls.

@gonfunko gonfunko requested a review from maribethb March 3, 2026 21:50
@gonfunko gonfunko requested a review from a team as a code owner March 3, 2026 21:50
@github-actions github-actions Bot added breaking change Used to mark a PR or issue that changes our public APIs. PR: feature Adds a feature labels Mar 3, 2026
Copy link
Copy Markdown
Contributor

@maribethb maribethb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know enough about web components so this seems plausible enough, but a couple questions.

if (
typeof window === 'undefined' ||
!window.CSSStyleSheet ||
injectionSites.has(root)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this return early now if the theme changes? i think the previous code allowed you to re-inject if the theme changed? not super familiar with this code though so could be misreading

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, yes! Updated it to be keyed by selector, so it only injects once per theme, and confirmed that fixes it.

injected = true;
if (!hasCss) {
export function inject(
container: HTMLElement,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again not too familiar with this code, but is it possible to have container be optional and the default value be getParentContainer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, but this is only called from Blockly.inject(), and is explicitly a no-op if called again. At the point Blockly.inject() calls it, the workspace doesn't know its injection div yet, so getParentContainer() wouldn't return it.

@gonfunko gonfunko merged commit 09d19d8 into v13 Mar 6, 2026
4 of 5 checks passed
@gonfunko gonfunko deleted the shadow-dom branch March 6, 2026 20:53
@microbit-robert
Copy link
Copy Markdown
Collaborator

microbit-robert commented Apr 13, 2026

@maribethb @gonfunko In the CSS cascade algorithm, adoptedStyleSheets are ordered after document.styleSheets, which affects precedence. As a result, where a Blockly class style was previously overridden by app-specific CSS without additional specificity by relying on document order, these overrides will no longer take effect. Consuming applications may need to bump the specificity of their overrides to restore custom styles.

This change will likely require an update to documentation.

Update: Possible additional CSS changes required due to inherited styles from the widget / dropdown div etc now being inside of the injectionDiv.

microbit-matt-hillsdon added a commit to microbit-matt-hillsdon/blockly that referenced this pull request May 20, 2026
…heets

Reverts the storage mechanism introduced in RaspberryPiFoundation#9611 (constructable
stylesheets via `adoptedStyleSheets`) while keeping the per-root
injection-site tracking that RaspberryPiFoundation#9611 added for shadow-DOM support.

Motivations:

- Safari 15.4 compatibility. `new CSSStyleSheet()` and
  `adoptedStyleSheets` require Safari 16.4+; the previous
  `!window.CSSStyleSheet` feature check was also nominal-only since
  the CSSStyleSheet interface is present in every browser.
- Cascade order. `adoptedStyleSheets` apply after `<style>`/`<link>`
  elements in the document, so Blockly's defaults silently overrode
  host stylesheets. Prepending a `<style>` to the head (or to the
  shadow root) restores the pre-RaspberryPiFoundation#9611 behavior where any author
  stylesheet declared later wins on specificity ties.

Trade-offs:

- Per-shadow-root CSS text is duplicated rather than shared via a
  single adopted sheet object. Negligible for typical use.
- `Css.register()` calls made after the first `inject()` no longer
  reach already-injected roots (same as RaspberryPiFoundation#9611's behavior); subsequent
  `inject()` calls into other roots still pick them up. Web-component
  consumers can legitimately register late, so this is preferred to
  reinstating the pre-RaspberryPiFoundation#9611 throw.

Fixes RaspberryPiFoundation#9876
microbit-matt-hillsdon added a commit to microbit-matt-hillsdon/blockly that referenced this pull request May 20, 2026
Reverts the storage mechanism introduced in RaspberryPiFoundation#9611 (constructable
stylesheets via `adoptedStyleSheets`) while keeping the per-root
injection-site tracking that RaspberryPiFoundation#9611 added for shadow-DOM support.

Motivations:

- Safari 15.4 compatibility. `new CSSStyleSheet()` and
  `adoptedStyleSheets` require Safari 16.4+; the previous
  `!window.CSSStyleSheet` feature check was also nominal-only since
  the CSSStyleSheet interface is present in every browser.
- Cascade order. `adoptedStyleSheets` apply after `<style>`/`<link>`
  elements in the document, so Blockly's defaults silently overrode
  host stylesheets. Prepending a `<style>` to the head (or to the
  shadow root) restores the pre-RaspberryPiFoundation#9611 behavior where any author
  stylesheet declared later wins on specificity ties.

Trade-offs:

- Per-shadow-root CSS text is duplicated rather than shared via a
  single adopted sheet object. Negligible for typical use.
- `Css.register()` calls made after the first `inject()` no longer
  reach already-injected roots (same as RaspberryPiFoundation#9611's behavior); subsequent
  `inject()` calls into other roots still pick them up. Web-component
  consumers can legitimately register late, so this is preferred to
  reinstating the pre-RaspberryPiFoundation#9611 throw.

Fixes RaspberryPiFoundation#9876
microbit-matt-hillsdon added a commit to microbit-matt-hillsdon/blockly that referenced this pull request May 20, 2026
Reverts the storage mechanism introduced in RaspberryPiFoundation#9611 (constructable
stylesheets via `adoptedStyleSheets`) while keeping the per-root
injection-site tracking that RaspberryPiFoundation#9611 added for shadow-DOM support.

Motivations:

- Safari 15.4 compatibility. `new CSSStyleSheet()` and
  `adoptedStyleSheets` require Safari 16.4+
- Cascade order. `adoptedStyleSheets` apply after `<style>`/`<link>`
  elements in the document, so Blockly's defaults silently overrode
  host stylesheets. Prepending a `<style>` to the head (or to the
  shadow root) restores the pre-RaspberryPiFoundation#9611 behavior where any author
  stylesheet declared later wins on specificity ties.

Trade-offs:

- Per-shadow-root CSS text is duplicated rather than shared via a
  single adopted sheet object. Negligible for typical use.
- `Css.register()` calls made after the first `inject()` no longer
  reach already-injected roots (same as RaspberryPiFoundation#9611's behavior); subsequent
  `inject()` calls into other roots still pick them up. Web-component
  consumers can legitimately register late, so this is preferred to
  reinstating the pre-RaspberryPiFoundation#9611 throw.

Fixes RaspberryPiFoundation#9876
maribethb pushed a commit that referenced this pull request May 20, 2026
Reverts the storage mechanism introduced in #9611 (constructable
stylesheets via `adoptedStyleSheets`) while keeping the per-root
injection-site tracking that #9611 added for shadow-DOM support.

Motivations:

- Safari 15.4 compatibility. `new CSSStyleSheet()` and
  `adoptedStyleSheets` require Safari 16.4+
- Cascade order. `adoptedStyleSheets` apply after `<style>`/`<link>`
  elements in the document, so Blockly's defaults silently overrode
  host stylesheets. Prepending a `<style>` to the head (or to the
  shadow root) restores the pre-#9611 behavior where any author
  stylesheet declared later wins on specificity ties.

Trade-offs:

- Per-shadow-root CSS text is duplicated rather than shared via a
  single adopted sheet object. Negligible for typical use.
- `Css.register()` calls made after the first `inject()` no longer
  reach already-injected roots (same as #9611's behavior); subsequent
  `inject()` calls into other roots still pick them up. Web-component
  consumers can legitimately register late, so this is preferred to
  reinstating the pre-#9611 throw.

Fixes #9876
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Used to mark a PR or issue that changes our public APIs. PR: feature Adds a feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants