Consistent Theming Across Quarto, ggplot2 and Observable


Ben Ewing


August 14, 2022

The Problem

When Quarto was formally released earlier this year, I knew it’d only be a matter of time until I felt the incessant need to transition my oft-neglected blog to this new framework. And so I have!

While the migration itself was straightforward, I struggled to commit to a single color theme. This was compounded by my preference for using a consistent color theme across the website and any blog post plots, meaning that any change in website theme would require a change to each post. Ideally, I would be able to change both the blog color theme and plot theming across all blog posts with a single switch.

The Solution

After some tinkering, I came up with the following solution:

  1. I gathered a few themes into .csv files, where each row is a color, and columns give the color’s name, hex, and role.
  2. I slightly modified the Minty bootswatch theme to use variables which define the role of a color (e.g. background, text, primary, etc. color). Note that not every color in a theme needs a role.
  3. I created jinjar compatible template files for ggplot2 and Quarto; I don’t use a template file for Observable, instead colors are saved in a simple .json file.
  4. I wrote a build script which reads in the theme .csv files, builds the templates, and writes them to a directory where Quarto and any blog posts know to look for themes.
  5. I modified the site _quarto.yml to run the theme build script every time the website is rendered.

Now any time I want to try a new theme, I just need to drop it into a .csv file, and make a small change to my theme building script! As a bonus, I also wrote a small script that takes the built ggplot2 template and creates a new themed favicon.

I’d love to hear any suggested improvements, feel free to tweet me!


Here are some examples showing consistency across each tool.


Themed badges.

Primary Secondary Success Danger Warning Info Light Dark


A simple iris plot.


iris |> 
  ggplot(aes(Sepal.Length, Sepal.Width, color = Species)) +
  geom_point() +
  scale_color_ip() +


Pass the iris data to Observable.

ojs_define(iris = iris)

Create a similar plot in Observable.

theme_obj = FileAttachment("../../themes/improper-prior-observable.json").json()
theme = theme_obj.scheme

  color: {
    legend: true,
    range: theme
  marks: [, {x: "Sepal.Length", y: "Sepal.Width", 
                               stroke: "Species"}),