Skip to content

✨ Add {anywidget} directive and AST node - #2602

Merged
stevejpurves merged 23 commits into
mainfrom
sp/any-widget
Feb 25, 2026
Merged

✨ Add {anywidget} directive and AST node#2602
stevejpurves merged 23 commits into
mainfrom
sp/any-widget

Conversation

@stevejpurves

@stevejpurves stevejpurves commented Dec 20, 2025

Copy link
Copy Markdown
Contributor

Upstreaming and adapting the AnyWidget directive code started by Curvenote, to provide a widget directive for MyST with first class AST support.

Note

See jupyter-book/myst-theme#795 for the theme counterpart.

See the tracking issue: #2603

@changeset-bot

changeset-bot Bot commented Dec 20, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e85d070

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
myst-frontmatter Patch
myst-directives Minor
myst-spec-ext Patch
myst-common Patch
myst-cli Patch
myst-config Patch
myst-parser Minor
myst-roles Minor
myst-to-html Minor
mystmd Patch
myst-migrate Patch
myst-transforms Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@stevejpurves stevejpurves changed the title ✨ directive first pass {widget} directive first pass Dec 20, 2025
@stevejpurves
stevejpurves marked this pull request as draft December 20, 2025 16:26
@agoose77

Copy link
Copy Markdown
Contributor

YAAAS! Thanks for sharing this, saves us having to reinvent the wheel!

Comment thread packages/myst-directives/src/widget.ts Outdated
Comment thread packages/myst-directives/src/widget.ts Outdated
stevejpurves and others added 2 commits February 5, 2026 08:56
Co-authored-by: Angus Hollands <goosey15@gmail.com>
@stevejpurves
stevejpurves marked this pull request as ready for review February 5, 2026 11:26
@choldgraf choldgraf added the enhancement New feature or request label Feb 10, 2026
@agoose77 agoose77 changed the title {widget} directive first pass {anywidget} directive first pass Feb 10, 2026

@choldgraf choldgraf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think that this is a cool enhancement and would definitely relieve some of the customization pressure for users that want to do custom stuff with JS and CSS.

Examples of things I'd want to try documenting (and using) with this:

I left a few comments to understand the scope you're aiming for in this PR. That said, I kinda feel like we should try to merge this in quickly so that we can play around with it and iterate in subsequent PRs (e.g. to add more docs, testing, etc once the implementation feels like it has fleshed out).

Should we make a release of mystmd and myst-theme, then merge this in and start iterating from there?

What do you think @stevejpurves ?

Comment thread packages/myst-directives/src/anywidget.ts Outdated
doc: 'Embed a MyST Widget (AnyWidget) component with data in the body',
arg: {
type: String,
required: true,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

do we always need to load a JS module? Could somebody use this just to load CSS onto the page or something? My first thought was that this could be a way for a plugin to load custom CSS even if it didn't want to change stuff with JS.

@stevejpurves stevejpurves Feb 12, 2026

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.

yes, anywidget always loads a JS module. The JS and CSS are rendered within a shadow DOM for isolation, so this is not a vehicle to get global CSS on the page. People can already do that via user style sheets.

@choldgraf choldgraf Feb 19, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Got it - so basically styling could be applied only to the DOM elements that are created by the directive.

People can already do that via user style sheets.

To clarify, users can, but plugins cannot.

Comment thread packages/myst-directives/src/anywidget.ts Outdated
Comment thread packages/myst-directives/src/anywidget.ts Outdated
@rowanc1

rowanc1 commented Feb 11, 2026

Copy link
Copy Markdown
Member

Embed custom styling for the handles in the github-handle-links plugin without the hacky abomination I'm using now

I don't think this PR is aimed at injecting styles in a general sense, but that is something that we should allow plugins to do somehow. This is for sure a hack — taking over the math node. :(

Any widget is mostly aimed at JS (e.g. your second example of the tables) — that could be an any widget + static document to a csv of something, or a map, or a dynamic chart, or basically anything. It skips the need for Jupyter a bit for the web-renderer, which is really nice — but it also can play with Jupyter as well.

Agree on getting this in and iterating, maybe with a beta flag in the docs when it shows up there.

@rowanc1 rowanc1 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Some other small comments.

Comment thread packages/myst-directives/src/anywidget.ts Outdated
Comment thread packages/myst-directives/src/anywidget.ts Outdated
Comment thread packages/myst-directives/src/anywidget.ts Outdated
@stevejpurves

Copy link
Copy Markdown
Contributor Author

@choldgraf yes a datatable widget is a nice example, albeit one that might be involved to create unless there is an existing out of the box js library you are looking at wrapping.

note it would be good to get examples in here: https://github.com/jupyter-book/example-widgets as a testing ground as well as more extensive docs

@stevejpurves

Copy link
Copy Markdown
Contributor Author

Thanks for all the feedback, I'll do another quick pass on this soon. After that I think some docs is the main blocker for merge - in like with @agoose77's definition of done in the issue.

@agoose77

agoose77 commented Feb 19, 2026

Copy link
Copy Markdown
Contributor

In bef3fbf I made some tweaks:

  • moved types to myst-spec-ext
  • deleted the static files (it's not in use right now)

I also made the options for CSS and ESM path or URL. I'll explain why.

I've realised that artifacts from GH Actions are not CORS-friendly! This is a real pain! It means that people will have to hack around with GitHub Pages if they want to serve content that we can fetch from another page.

Instead, I think we should take this as a signal that local files are important — the same system for loading local ESM is useful in cacheing remote ESM at build time, which ignores CORS.

I have a prototype locally but it's ugly and needs a bit of work. I will push it here to unblock people (42b8ac4), but feel free to delete/redo it as needs be.

Long term I think we should use this as a cue to support path option types in directives etc, so that we can generalise this pattern (or, perhaps better, a utility to "declare" a static asset) in the directive. However, for now it's probably fine to special case widgets.

@agoose77

Copy link
Copy Markdown
Contributor

Here's a preview of the docs:
image

@choldgraf you've spent a lot of time in our docs recently. I didn't spend a huge amount of time here because I time-boxed to get the whole PR forward. Could I ask for a glance over?

@stevejpurves can you sign off the changes?

@stevejpurves stevejpurves changed the title {anywidget} directive first pass {widget} directive first pass Feb 24, 2026
model = JSON5.parse(body);
} catch (e) {
json = { error: 'Invalid JSON5/JSON supplied.' };
model = { error: 'Invalid JSON5/JSON supplied.' };

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.

I think this is a good step 👍🏼


return [
{
type: 'anywidget',

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.

should we just be going with widget?

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.

I'm leaning that way too, actually!

let model: ReturnType<typeof JSON5.parse>;
if (data.body === undefined) {
json = {};
model = {};

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.

👍🏼

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.

thanks for getting to this @agoose77

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.

My pleasure!

@stevejpurves stevejpurves changed the title {widget} directive first pass {anywidget} directive first pass Feb 24, 2026
@stevejpurves

Copy link
Copy Markdown
Contributor Author

@agoose77 feedback on changes:

  • from string -> url/file: I think the file copying change is definitely what we want, perhaps there should be an option which is an escape hatch to allow it to remain remote only, when remote? This implementation is good enough for this cut (but also if generalised could solve the other "get static files into the _build folder" issues/requests).
  • just related to that should we support inline esm/css like this? or the local files are good enough/better? I feel now like there is no need? question for next PR though!
image

@stevejpurves

Copy link
Copy Markdown
Contributor Author

I am testing and seeing issues when using the jupyter-book/example-widgets repo;

image

I am using the latest from this branch jupyter-book/myst-theme#795

@agoose77 did you get the examples repo to work as expected after your change?

@agoose77

Copy link
Copy Markdown
Contributor

@stevejpurves and I discussed this via DM on Discord — I forgot an await, which introduced a race race condition.

from string -> url/file: I think the file copying change is definitely what we want, perhaps there should be an option which is an escape hatch to allow it to remain remote only, when remote? This implementation is good enough for this cut (but also if generalised could solve the other "get static files into the _build folder" issues/requests).

Yes, I definitely think this should be generalised. I want to tackle that separately, so that it doesn't block this work. See more here: #2714!

just related to that should we support inline esm/css like this? or the local files are good enough/better? I feel now like there is no need? question for next PR though!

I think ESM and CSS in-document is an antipattern for us. I'm also thinking about whether the same is true for JSON. For simple widgets, inline JSON is probably preferable, but for bigger ones we don't want huge 3D model definitions in the bundle! We can probably add a separate model-file directive argument for that and combine it with a transform that moves JSON to disk. I think it's OK to hold off on that and introduce backwards compatibility down the road?

@stevejpurves

stevejpurves commented Feb 24, 2026

Copy link
Copy Markdown
Contributor Author

Yes, I definitely think this should be generalised. I want to tackle that separately, so that it doesn't block this work. See more here: #2714!

💯 on that @agoose77 and thanks for the replies on DM

@choldgraf

choldgraf commented Feb 25, 2026

Copy link
Copy Markdown
Collaborator

I took a pass at the documentation to build on what @agoose77 has added (as a compliment to the docs work I added over in the myst-theme PR). I think overall this is good enough to ship as-is, especially now that we have local modules, which IMO makes this much more powerful because you can wrap other libraries in your own anywidget logic).

When I did that, I also updated the nomenclature to use the phrase MyST Widgets and then Widgets (since it seemed like both @agoose77 and @stevejpurves were in favor, and I also think this is a good idea). I have not changed any filenames or code, because I don't want to clobber somebody's work if y'all have working branches locally (I assumed the docs were safer to change).

IMO, this is good enough to ship right now, if we do:

  • label it as an alpha feature with an admonition at the top (ie, no promises that the API or key functionality will be stable, but we quickly hope to converge on beta-level stability)
  • (optionally) create an issue where people can provide comments etc, but IMO we could also just ask people to open issues if they hit problems

That way we can invite experimentation from power users without setting up the assumption of stability.

I think this is a really cool feature, and leans into our strategy of making myst a more hackable/pluggable platform for developers!

@choldgraf choldgraf changed the title {anywidget} directive first pass ✨ Add {anywidget} directive and AST node Feb 25, 2026
@stevejpurves

Copy link
Copy Markdown
Contributor Author

I think we can merge as soon as CI is green

@stevejpurves

Copy link
Copy Markdown
Contributor Author

I am approving this based on out of band convos with @agoose77

@stevejpurves
stevejpurves merged commit 7b989d4 into main Feb 25, 2026
9 checks passed
@stevejpurves
stevejpurves deleted the sp/any-widget branch February 25, 2026 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants