Custom Branded Logos for JupyterLab and RetroLab (Jupyter notebook v.7)

Of the three main blockers in terms of look feel that I’ve used as an excuse to not to start thinking about moving course materials over to JupyterLab/RetroLab, I’ve now got hacky extensions for styling notebooks, empinken style, and enable cell run status indicators. The next one is purely cosmetic – adding custom logos.

The proper way to do this (?!) is probably to use a custom theme. See for an example of adding custom logos to to a custom theme.

Whilst it doesn’t appear that there is straightforward “parameter configurable” way of doing this, and there is zero help on the various forums and issues trackers for anyone who does want to achieve this (because believe it or not, the setting does matter sometimes, and learners particularly, often benefit from thinking they’re in a “safe space” that is suggested by branded environments), I finally had a poke around for some hacky ways of doing this.

It does, of course, require all the pain of building an extension, but we don’t need much more than some simple CSS and some simple JS, so we can get away with using the the JupyterLab Javascript cookiecutter extension.

To open the cookiecutter files into a pre-existing directory, such as a one created by cloing a remote Gihub repo onto your desktop, run the command:

cookiecutter -fs

You can then set up the environment:

cd my_extension/`

python -m pip install .

And do a test build:

jlpm run build && python3 -m build

You can then install the resulting package:

pip3 install ./dist/my_extension-0.1.0-py3-none-any.whl

To customise the logos, in the ./style/base.css file, we can hide the default JupyterLab logo and add our own:

#jp-MainLogo {
    background-image: url(./images/OU-logo-36x28.png);
    background-repeat: no-repeat;

#jp-MainLogo > svg {
    visibility: hidden;

#jp-RetroLogo {
    background-image: url(./images/OU-logo-53x42.png);
    background-repeat: no-repeat;

#jp-RetroLogo > svg {
    visibility: hidden;

The images should be place in a new ./style/images/ folder.

To have a go at hacking the favicon (which works on a “full server, ish, but not in JupyterLite?), we need some simple Javascript in ./style/index.js:

import './base.css';

// Via:

let head = document.head || document.getElementsByTagName('head')[0];

let link = document.createElement('link')
let oldLink = document.getElementsByClassName('favicon');
link.rel = 'icon';
link.type = 'image/x-icon';
link.href = '';
if (oldLink) {
    link.classList = oldLink[0].classList;

I’m not sure how to reference a local, statically packed favicon shipped with the extension, so for now I pull in a remote one.

Rebuild the extension, and reinstall it:

jlpm run build && python3 -m build && pip3 install --upgrade./dist/my_extension-0.1.0-py3-none-any.whl

To make it easier to distribute, I remove the dist/ element from the .gitignore file and push everything to a repo.

The following Github action acan be manually triggered to build a JupyterLite enviornment pushed to Github Pages (in your Github repo, you need to go to Settings > Pages and select the gh-pages branch as the target for the site.

I include the custom extension in the JupyerLite build via a requirements-jupyterlite.txt file which includes the following:


name: JupyterLite Build and Deploy

    types: [published]

    runs-on: ubuntu-latest
      - name: Checkout
        uses: actions/checkout@v2
      - name: Setup Python
        uses: actions/setup-python@v2
          python-version: 3.8
      - name: Install the dependencies
        run: |
          python -m pip install -r requirements-jupyterlite.txt
      - name: Build the JupyterLite site
        run: |
          cp content
          jupyter lite build --contents content
      - name: Upload (dist)
        uses: actions/upload-artifact@v2
          name: jupyterlite-demo-dist-${{ github.run_number }}
          path: ./_output

    if: github.ref == 'refs/heads/main'
    needs: [build]
    runs-on: ubuntu-latest
      - name: Checkout
        uses: actions/checkout@v2.3.1
      - uses: actions/download-artifact@v2
          name: jupyterlite-demo-dist-${{ github.run_number }}
          path: ./dist
      - name: Deploy
        uses: JamesIves/github-pages-deploy-action@4.1.3
          branch: gh-pages
          folder: dist

A demo of the extension can then be tried purely withing the browser via JupyterLite.

Repo is here:

Demo is here:

Files changed compared to cookiecutter generated pages here:

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: