Skip to content

Instantly share code, notes, and snippets.

@bollwyvl
Last active January 23, 2026 15:05
Show Gist options
  • Select an option

  • Save bollwyvl/ab6603e89423ff993e63bb4ec1a10582 to your computer and use it in GitHub Desktop.

Select an option

Save bollwyvl/ab6603e89423ff993e63bb4ec1a10582 to your computer and use it in GitHub Desktop.
Using Jupyter Widgets with Arbitrary JavaScript and HTML
Display the source blob
Display the rendered blob
Raw
{
"metadata": {
"kernelspec": {
"display_name": "Python (Pyodide)",
"language": "python",
"name": "python"
},
"language_info": {
"codemirror_mode": {
"name": "python",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8"
}
},
"nbformat_minor": 5,
"nbformat": 4,
"cells": [
{
"id": "f5e123c1-df85-4c06-b9e9-ed166259fbc5",
"cell_type": "markdown",
"source": "# Using Jupyter Widgets with Arbitrary JavaScript and HTML\n\nThe legacy JavaScript API for the Jupyter notebook presented many problems: it relied on page-level constants, specific to the client implementation.\n\nThis approach shows using Jupyter Widgets in the kernal and DOM as a JSON-based backhaul.",
"metadata": {}
},
{
"id": "a99bc8c8-a7fb-4bae-a954-1c0852063195",
"cell_type": "code",
"source": "%pip install ipywidgets\nfrom datetime import datetime\nfrom ipywidgets import Textarea, DatetimePicker, VBox, HBox, Text, HTML\nimport json",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": 116
},
{
"id": "e7b74666-bcec-4da7-b583-a832d9587588",
"cell_type": "markdown",
"source": "## Build the JSON bus widgets",
"metadata": {}
},
{
"id": "2141fbad-8f31-42c9-9ef0-3c207de4990e",
"cell_type": "code",
"source": "from_js, to_js = [Textarea(description=f\"{d} JS\") for d in [\"from\", \"to\"]]\nfrom_js.add_class(\"my-from-js\")\nto_js.add_class(\"my-to-js\")\nHBox([from_js, to_js])",
"metadata": {
"trusted": true
},
"outputs": [
{
"execution_count": 117,
"output_type": "execute_result",
"data": {
"text/plain": "HBox(children=(Textarea(value='', description='from JS', _dom_classes=('my-from-js',)), Textarea(value='', des…",
"application/vnd.jupyter.widget-view+json": {
"version_major": 2,
"version_minor": 0,
"model_id": "3cc94426dfa44741a6ae3b67497d08cd"
}
},
"metadata": {}
}
],
"execution_count": 117
},
{
"id": "15bd634e-cab8-46e0-9c45-1c6fe378d978",
"cell_type": "markdown",
"source": "## Build some \"real\" widgets\n\nThese demonstrate some rich inputs and outputs.",
"metadata": {}
},
{
"id": "9b495b9f-a764-408e-8d4f-4ce2d7be5033",
"cell_type": "code",
"source": "last_click = DatetimePicker(description=\"last click\")\nbutton_label = Text(\"click me\", description=\"button label\")",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": 118
},
{
"id": "11c85c89-cc7b-4b10-9ecd-02b4118ee396",
"cell_type": "markdown",
"source": "## Connect events\n\nThese will use JSON as the intermediate, instead of raw Python/JS equivalents of arbitrary code execution.",
"metadata": {}
},
{
"id": "3b5a1c2d-669d-4027-8262-b0295ac5ff5b",
"cell_type": "code",
"source": "def on_from_js(*_):\n new = json.loads(from_js.value or '{}')\n new_click = new.get(\"last_click\")\n last_click.value = datetime.fromisoformat(new_click) if new_click else None\n\ndef send_js(*_):\n to_js.value = json.dumps({\"button_label\": button_label.value})\n\nfrom_js.observe(on_from_js, \"value\")\nbutton_label.observe(send_js, \"value\")",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": 119
},
{
"id": "e64f78d5-1f64-4779-9c0b-50fddcceb512",
"cell_type": "markdown",
"source": "## Show custom HTML\n\nThis uses the `%%html` magic, but anything that generates a rich HTML output could work.",
"metadata": {}
},
{
"id": "96c235e2-202a-42f2-bada-97928b00d7d7",
"cell_type": "code",
"source": "%%html\n<button class=\"my-button\">click me</button>\n<script>\n;(function (){\n function sendPy(data) {\n fromJs = document.querySelector('.my-from-js textarea');\n fromJs.value = JSON.stringify(data);\n fromJs.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));\n }\n function onToJs() {\n let toJs = document.querySelector('.my-to-js textarea');\n let data = JSON.parse(toJs.value);\n button.textContent = data[\"button_label\"];\n }\n let button = document.querySelector('.my-button');\n button.addEventListener(\"click\", () => sendPy({last_click: (new Date()).toISOString()}));\n // polling is not great, but here we are\n setInterval(onToJs, 1000);\n console.log('loaded');\n}).call(this);\n</script>",
"metadata": {
"trusted": true
},
"outputs": [
{
"output_type": "display_data",
"data": {
"text/plain": "<IPython.core.display.HTML object>",
"text/html": "<button class=\"my-button\">click me</button>\n<script>\n;(function (){\n function sendPy(data) {\n fromJs = document.querySelector('.my-from-js textarea');\n fromJs.value = JSON.stringify(data);\n fromJs.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));\n }\n function onToJs() {\n let toJs = document.querySelector('.my-to-js textarea');\n let data = JSON.parse(toJs.value);\n button.textContent = data[\"button_label\"];\n }\n let button = document.querySelector('.my-button');\n button.addEventListener(\"click\", () => sendPy({last_click: (new Date()).toISOString()}));\n // polling is not great, but here we are\n setInterval(onToJs, 1000);\n console.log('loaded');\n}).call(this);\n</script>\n"
},
"metadata": {}
}
],
"execution_count": 120
},
{
"id": "de8dcbab-521e-440d-b35c-6e46c724e60e",
"cell_type": "code",
"source": "HBox([button_label, to_js, from_js, last_click])",
"metadata": {
"trusted": true
},
"outputs": [
{
"execution_count": 121,
"output_type": "execute_result",
"data": {
"text/plain": "HBox(children=(Text(value='click me', description='button label'), Textarea(value='', description='to JS', _do…",
"application/vnd.jupyter.widget-view+json": {
"version_major": 2,
"version_minor": 0,
"model_id": "d9981a2fb32d4792ac6ed3d273cfd4c8"
}
},
"metadata": {}
}
],
"execution_count": 121
},
{
"id": "c291b717-e4be-47bf-8de3-6cb6b6fbba1e",
"cell_type": "markdown",
"source": "The \"bus\" widgets need to be actually be added to the DOM, but can be visually hidden.",
"metadata": {}
},
{
"id": "dd5423b2-aa5d-47fd-9bbb-9333cd7c1172",
"cell_type": "code",
"source": "to_js.layout.display = from_js.layout.display = \"none\"",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": 122
},
{
"id": "47ac5b8b-3631-4df7-9337-b826522cc05f",
"cell_type": "markdown",
"source": "## Alternatives\n\n- for very complex HTML/CSS, using an `iframe` may work better with a [`postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) approach\n- some cases can use [`ipylab`](https://github.com/jtpio/ipylab) to further integrate with Jupyter clients, such as:\n - building new UI panels\n - using application-level commands and icons",
"metadata": {}
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment