Skip To Article

MyST Widgets let you include JavaScript applets into the content of a MyST document. They are self-contained, and can be shared across MyST projects (e.g. via an {embed} directive). They are designed to be simple to develop and use.

MyST widgets follow the anywidget specification, a toolset for authoring reusable web-based widgets for interactive computing environments. Widgets written for Jupyter anywidget can also work in MyST sites.

Here’s an example that creates a clickable button:

Confetti Example
anywidget - Unknown Directive

Overview of MyST Widgets

MyST Widgets are defined with the following two things:

  1. A JavaScript module. This defines the core logic of the widget.

  2. A stylesheet (optional). This applies styles to the widget.

The Widget module exports a render function that operates on two arguments:

  1. model: contains the widget state, and can be used to update that state in your module.

  2. el: a DOM element that your module can modify, and that will be inserted into the page.

The el DOM of a widget is a Shadow DOM. It is not “owned” by React in the same way that the rest of the page is, and so it is a safer way to generate arbitrary HTML and CSS as part of your MyST document.

Simple widget structure

A widget module must export a default object with a render function:

my-widget.mjs
function render({ model, el }) {
  // Build your UI and append it to `el`
}
export default { render };

You can then use it in a MyST document via the {widget} directive like so:

```{anywidget} ./my-widget.mjs
```

The render function receives two arguments:

model

A state object initialized from the JSON body of the {anywidget} directive.

  • model.get(key) — read a state value

  • model.set(key, value) — update a state value (triggers change events)

  • model.on('change:<key>', callback) — react to state changes

el

An empty DOM element where your widget should render its content. Add and modify this DOM and it will show up on the page.

The render function can optionally return a cleanup function that is called when the widget is removed from the page.

Use the widget model

The following simple widget example demonstrates how to use the get, set, and on functions of the model object.

Widgets contain a model (accessible via the model argument) that contains their state. This can be arbitrary key/value pairs that are used in the final display of the widget.

You can instantiate a widget with model values via JSON provided in the directive body. For example, this would create a widget with count: 0.

```{anywidget} my-widget.mjs
{
  "count": 0
}
```
  • Running model.get('count') returns 0 at widget startup.

  • Running model.set('count', 10) updates its value to 10.

  • Running model.on('change:count`, (count) => el.button.innerHtml = `Count is `${count}`) will update the button’s HTML each time count changes.

An example widget

Below is a simple widget example that ties together the logic above:

example-widget.mjs
function render({ model, el }) {
  // Setup quick-access to state
  const getCount = () => model.get('count');
  const setCount = (count) => model.set('count', count);

  // Create button
  let btn = document.createElement('button');
  btn.classList.add('counter-button');
  btn.innerHTML = `count is ${getCount()}`;

  // Handle button click
  btn.addEventListener('click', () => {
    setCount(getCount() + 1);
    model.save_changes();
  });
  // Update text when count changes
  model.on('change:count', () => {
    btn.innerHTML = `count is ${getCount()}`;
  });
  el.appendChild(btn);

  // Destructor to clean-up when MyST is finished with us!
  return () => btn.remove();
}
export default { render };

This creates the following button:

Naked Button Example
anywidget - Unknown Directive
{
  "count": 0
}

Add style to widgets

There are three ways you can style widgets.

Style with a CSS stylesheet

You can create your own stylesheet (.css file) and link it to the Widget output. For example, create a stylesheet like the following:

example-widget-style.css
.counter-button {
  font-family: 'Open Sans', sans-serif;
  font-size: 16px;
  letter-spacing: 2px;
  text-decoration: none;
  text-transform: uppercase;
  color: #000;
  cursor: pointer;
  border: 3px solid;
  padding: 0.25em 0.5em;
  box-shadow:
    1px 1px 0px 0px,
    2px 2px 0px 0px,
    3px 3px 0px 0px,
    4px 4px 0px 0px,
    5px 5px 0px 0px;
  position: relative;
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
}

.counter-button:active {
  box-shadow: 0px 0px 0px 0px;
  top: 5px;
  left: 5px;
}

And then add it to the widget like so:

```{anywidget} ./example-widget.mjs
:css: ./example-widget-style.css
{
  "count": 0
}
```
Styled Button Example
anywidget - Unknown Directive
:css: ./example-widget-style.css

{
  "count": 0
}

These styles are added to a Shadow DOM that isolates the widget from the rest of the page styles.

Add style attributes to DOM elements

If you wish to keep your styles entirely contained within the widget module, you can assign them directly to elements you create. For example:

function render({ model, el }) {
  const btn = document.createElement('button');
  Object.assign(btn.style, {
    padding: '0.4em 0.8em',
    border: '2px solid #333',
    borderRadius: '4px',
  });
  el.appendChild(btn);
}

This is the simplest approach, but note that inline styles have high specificity - they cannot be overridden by a CSS stylesheet unless you use !important rules.

Style with a stylesheet within the DOM

Combine the two approaches above by injecting a <style> tag into the el DOM and attaching a class to your widget elements. This allows you to use CSS styling that a user could over-ride more easily if they wish.

function render({ model, el }) {
  const style = document.createElement('style');
  style.textContent = `.my-button { padding: 0.4em 0.8em; border: 2px solid #333; }`;
  el.appendChild(style);

  const btn = document.createElement('button');
  btn.classList.add('my-button');
  el.appendChild(btn);
}

Return a cleanup function

The render function can optionally return a cleanup function. This is called when the widget is removed from the page — for example, when a user navigates to a different page. Use it to clean up any resources your widget created, such as if you create JavaScript timers.

For example, the following cleanup function cleans up a timer that would otherwise run after the widget was destroyed:

function render({ model, el }) {
  // UI
  const span = document.createElement('span');
  el.appendChild(span);

  // Update UI from model
  model.on('change:timestamp', () => {
    el.innerText = model.get('timestamp');
  });

  // Update model from events
  let timeoutID;
  const step = () => {
    model.set('timestamp', new Date().toLocaleTimeString());
    timeoutID = setTimeout(step, 1000);
  };
  step();

  // Clean up when the widget is removed from the page
  return () => clearTimeout(timeoutID);
}
export default { render };

Security and best practices

Widgets have full access to the page’s document, which means they can select, modify, or remove any element on the page, not just their own el DOM. We do not recommend modifying elements outside of the el DOM. Making changes outside of the el dom is not a supported workflow and can cause unpredictable behavior.

Future Development

In the future, we are looking to find ways to integrate widgets with core MyST AST rendering. This would make it possible to create widgets that act on the MyST AST, such as table filtering, or galleries. Stay tuned!

MyST MarkdownMyST Markdown
Community-driven tools for the future of technical communication and publication, part of Jupyter.