Notes on the JupyterLab Notebook HTML DOM Model, Part 1: Rendered Markdown Cells

For a great review of the DOM structure of the JupyterLab UI, see [Jupyter Accessibility docs](

I finally relented, and after going through the apparent overkill of building a JupyterLab extension simply to sneak a custom CSS file into the distribution, I posted a query onto Stack Overflow to see if I could crib enough to have a go at a JupyterLab extension that replicates my classic notebook empinken extension that uses toolbar buttons to toggle persistent state on a notebook that can be used to colour highlight cells in particular ways.

The query was comprehensivley answered and gave me all the clues I needed to make a stab at it. I’ll post a note about the extension when I finish a first draft of it, along with some reflections about the process…

The empinken extension I’m interested in building essentially requires four components:

  • buttons to toggle empinken style state on a cell;
  • a means of persisting empinken style state, eg via cell metadata or cell tags;
  • a means of styling cells appropriately (a combination of HTML tag classes and CSS style rules);
  • a means of adding class attributes to the DOM based on the cell empinken style state.

There is an optional fifth consideration in the original, which was a simple YAML file defined control panel exposed by the classic notebook jupyter_nbextensions_configurator. In the case of the empinken extension, this let you set cell colours, with the whole configurator defined by a simple YAML file:

I don’t think JupyerLab supports any similar extension configurator tool: you have to write your own and find somewhere to display it.

I haven’t yet figured out the styling, so the rest of this post will be a transcript of how notebook cells seem to be represented in the DOM. (I haven’t spotted any docs on this? If you know of any, please post a link in the comments.)

So let’s get started… (I’m only going to focus on markdown and code cells for now, because they are generally the only ones I tend to use…)

A Note on Tags

Tags are used by certain extensions and document processing tools (Jupytext, Jupyter Book, etc.) to modify how documents are processed and rendered.

In some cases, it may be that JupyterLab extensions can be used to modify how cells are rendered on the basis of cell tags.

In other cases, it might be that document processors support conventions within a cell for extended rendering. For example, in Jupyter Book, directive blocks can be rendered using styled HTML block. (The JupyterLab Myst extension can used an extended markdown parser to render these elements in a notebook, though I have found it can be a bit slow to render the elements…) However, it is also possible to construct document processing pipelines that use cell tags to denote, eg a certain directive block, and then generate an intermediate MyST document with MyST style directives that is parsed by the Jupyter Book processor. In such a case, the notebook tag would be available to a JupyterLab extension to tune the rendering of the cell. (For a crude example, see innovationOUtside/ou-jupyter-book-tools.)

Markdown Cell Structure

The .ipynb JSON format for a markdown cell is defined as follows:

  "cell_type" : "markdown",
  "metadata" : {
       "tags": ["mytag", "my-other-tag"]
  "source" : ["some *markdown*"],

When rendered as a JupyterLab cell, the following structure is evident:

<div class="lm-Widget p-Widget jp-Cell jp-MarkdownCell jp-Notebook-cell jp-mod-rendered">
    <div class="lm-Widget p-Widget jp-CellHeader jp-Cell-header"></div>
    <div class="lm-Widget p-Widget lm-Panel p-Panel jp-Cell-inputWrapper">
        <div class="lm-Widget p-Widget jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
            <div class="jp-Collapser-child"></div>
        <div class="lm-Widget p-Widget jp-InputArea jp-Cell-inputArea">
            <div class="lm-Widget p-Widget jp-InputPrompt jp-InputArea-prompt"></div>
            <div class="lm-Widget p-Widget jp-CodeMirrorEditor jp-Editor jp-InputArea-editor lm-mod-hidden p-mod-hidden flash-effect" data-type="inline">
                <div class="CodeMirror cm-s-jupyter CodeMirror-wrap">
                    <div style="overflow: hidden; position: relative; width: 3px; height: 0px; top: 5px; left: 74.4375px;"><textarea autocorrect="off" autocapitalize="off" spellcheck="false" tabindex="0" style="position: absolute; bottom: -1em; padding: 0px; width: 1000px; height: 1em; outline: none;"></textarea></div>
                    <div class="CodeMirror-vscrollbar" tabindex="-1" cm-not-content="true">
                        <div style="min-width: 1px; height: 0px;"></div>
                    <div class="CodeMirror-hscrollbar" tabindex="-1" cm-not-content="true">
                        <div style="height: 100%; min-height: 1px; width: 0px;"></div>
                    <div class="CodeMirror-scrollbar-filler" cm-not-content="true"></div>
                    <div class="CodeMirror-gutter-filler" cm-not-content="true"></div>
                    <div class="CodeMirror-scroll" tabindex="-1">
                        <div class="CodeMirror-sizer" style="margin-left: 0px; margin-bottom: -15px; border-right-width: 35px; min-height: 61px; padding-right: 0px; padding-bottom: 0px;">
                            <div style="position: relative; top: 0px;">
                                <div class="CodeMirror-lines" role="presentation">
                                    <div role="presentation" style="position: relative; outline: none;">
                                        <div class="CodeMirror-measure"><pre class="CodeMirror-line-like"><span>xxxxxxxxxx</span></pre></div>
                                        <div class="CodeMirror-measure"></div>
                                        <div style="position: relative; z-index: 1;"></div>
                                        <div class="CodeMirror-cursors" style="visibility: hidden;">
                                            <div class="CodeMirror-cursor" style="left: 74.4375px; top: 0px; height: 17px;">&nbsp;</div>
                                        <div class="CodeMirror-code" role="presentation"><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">A markdown cell.</span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;"><span cm-text="">​</span></span></pre><pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;">Over several lines.</span></pre></div>
                        <div style="position: absolute; height: 35px; width: 1px; border-bottom: 0px solid transparent; top: 61px;"></div>
                        <div class="CodeMirror-gutters" style="display: none; height: 96px;"></div>
            <div class="lm-Widget p-Widget jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput" data-mime-type="text/markdown">
                <p>A markdown cell.</p>
                <p>Over several lines.</p>
    <div class="lm-Widget p-Widget jp-CellFooter jp-Cell-footer"></div>

In what follows, the colouring of the block elements corresponds to a particular CSS element scope:

Let’s start at the top, the <div class="lm-Widget p-Widget jp-Cell jp-MarkdownCell jp-Notebook-cell jp-mod-rendered"> element that contains everything:

Note the padding that extends around the element.

This element contains three child elements: a header (which appears to be empty – what is this for?), the body (or panel), and a footer (which also appears to be empty; again, what is it for and is there any content that can easily be rendered into it?):

If the cell is selected, the top level block adds a couple of extra classes: jp-mod-active jp-mod-selected. The selected cell is highlighted:

And what does the panel element cover?

Let’s look inside the panel element:

This element has two child elements: a collapser, and an input area.

Let’s look at the collapser first, which is quite a simple element:

The collapser elements cover the gutter area that is highlighted when a markdown cell is selected (the child element appears to cover the same extent):

The next element is the inputarea into which the notebook source is eventually parsed and rendered. This appears to cover everything to the right of the collapser:

The element itself contains three elements, an InputPrompt, an Editor and a MarkdownOutput/RenderedMarkdown element:

In a rendered markdown cell, the editor cell (which has quite a complex internal structure) does not appear to have a rendered extent. When the markdown cell is put into edited mode, the MarkdownOutput element seems to disappear from the DOM.

This might be a hassle if we are tag styling cells: we would need to ensure that when the MarkdownOutput element is added back to the DOM the element gets classed appropriately. Which means we would have to set a watcher on the cell.

The extent of the MarkdownOutput element (which is the same as the extent of the editor element when in edit mode) is everything to the right of the input area. Note the padding extends to the left and further to the right.

The MarkdownOutput element simply contains the rendered markdown HTML:

Each paragraph line then has its own lower margin:

In the next post in this series, we’ll consider the code cell DOM elements.

Author: Tony Hirst

I'm a Senior Lecturer at The Open University, with an interest in #opendata policy and practice, as well as general web tinkering...

%d bloggers like this: