Skip to main content
AI in Production 2026 is now open for talk proposals.
Share insights that help teams build, scale, and maintain stronger AI systems.
items
Menu
  • About
    • Overview 
    • Join Us  
    • Community 
    • Contact 
  • Training
    • Overview 
    • Course Catalogue 
    • Public Courses 
  • Posit
    • Overview 
    • License Resale 
    • Managed Services 
    • Health Check 
  • Data Science
    • Overview 
    • Visualisation & Dashboards 
    • Open-source Data Science 
    • Data Science as a Service 
    • Gallery 
  • Engineering
    • Overview 
    • Cloud Solutions 
    • Enterprise Applications 
  • Our Work
    • Blog 
    • Case Studies 
    • R Package Validation 
    • diffify  

Porting a Shiny App to Observable Framework: Part 2

Author: Tim Brock

Published: January 30, 2025

tags: r, shiny, observable, markdown, javascript, html, css, d3, ui

Preamble

This post, Part 2 in a series of two, looks at styling and deploying the Observable Framework app we built in Part 1. Codeblocks with burgundy backgrounds refer to specifc tagged commits in the accompanying GitHub repositiory.

Join us for the next installment of our Shiny in Production conference! For more details, check out our conference website!

Styling the App with CSS

We can add a stylesheet by referencing it through the “style” property in the configuration file: observable.config.js. That config file can be used to define various attributes for our project, including what title and favicon should be displayed in the browser tab, where the root of the source code is (root: "src") and where, relative to that root, the stylesheet is stored (style: "style/style.css").

You can go crazy here with your CSS or keep it simple. Since this is just meant as a quick demonstration we’ll do the latter: we’ll tweak the appearance of controls, add Jumping Rivers fonts and colours and rearrange the layout for wider screens:

src/style/style.css
@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap");


body {
  font-family: "Outfit", sans-serif;
  position: relative;
  color: #0c293d;
  background-color: #fcfbfa;
}

main {
  display: grid;
  justify-content: center;
  align-items: center;
  column-gap: 3em;
  grid-template-columns: 350px 500px;
  grid-template-areas:
    "title title"
    "controls chart"
    "controls count";
}

main > h1 {
  font-weight: 600;
  grid-area: title;
  text-align: center;
}

main > div {
  display: none;
}

main > div:has(form) {
  display: unset;
  grid-area: controls;
  padding-top: 1em;
}

main > div:has(figure) {
  display: flex;
  justify-content: center;
  grid-area: chart;
}

main > p {
  grid-area: count;
  text-align: center;
}

input[type="number"] {
  text-align: right;
}

main form[class^="inputs"]:has(input[type="number"]) {
  display: inline-flex;
  flex-direction: column;
  width: calc(50% - 1em);
  margin-right: 1em;
  margin-bottom: 1em;
}

main form[class^="inputs"]:has(input[type="number"]) label {
  width: 100%;
}

main form[class^="inputs"]:has(select, input[type="range"], input[type="text"], input[type="radio"]) {
  display: flex;
  width: 100%;
  flex-direction: column;
  margin-bottom: 0.5em;
}

main form[class^="inputs"]:has(select, input[type="range"], input[type="text"], input[type="radio"]) > * {
  width: 100%;
}

[aria-label="tip"] {
  fill-opacity: 0.8;
}

[aria-label="tip"] text tspan:first-child {
  font-weight: bold;
}

@media (max-width: 950px) {
  main {
    padding: 0 1em;
    grid-template-columns: unset;
    grid-template-areas:
      "title"
      "controls"
      "chart"
      "count";
  }
}
Screenshot showing the final version of the app with all styles applied

To keep things succinct, our stylesheet makes use of the relatively new (Firefox was the last major browser to support this in late 2023) CSS :has pseudoclass. If you need to support older browsers you’d have to find another way of doing things. Using :has allows us, for example, to target elements with specific descendants without relying too much on the generated classes remaining unchanged and without manually adding explicit ids or classes to those target elements.

git switch --detach styles

Tidying Up

All that’s left now to “complete” our app is to tidy up a few loose ends, removing some comments and files that are no longer helpful. This amounts to:

  • Updating the README
  • Updating and pruning the observablehq.config.js configuration file
  • Deleting a JavaScript file we don’t use
  • Removing an irrelevant image file
git switch --detach tidy

Deployment

You can build a static version of the app using:

npm run build

This is only static in the sense that the output files can be served by essentially any old server; there’s no need to have a server that can process the R scripts or (Python or rust etc) or build HTML from markdown. You won’t get the hot reloading that you get with npm run dev as you make changes but the output - that by default gets dumped in a dist/ directory - can be deployed almost anywhere. That includes on Observable cloud, which is super-easy to do. Run

npm run deploy

You’ll be asked to sign in if you haven’t already: you can use your GitHub credentials for this, if you like. After that you’ll get a few simple questions to answer about naming, visibility and the like and then - within a minute or so - it’s done, with a link to the deployed app printed to the terminal. View our app. The Observable website has further instructions if you want to go down the route of automated deploys and/or GitHub actions.

git switch --detach deploy

Final Thoughts

This was a fun thing to try and didn’t take especially long to implement. The way you can add scripts for data generation and things “just work” is really neat. Having the whole of d3 and Observable Plot available without having to do explicit installs and imports is also helpful. Because of these things, setup of a new project can be really quick. Deployment to Observable cloud is also super speedy and other deployment targets shouldn’t be difficult, either.

On the negative side I’m not convinced by the use of markdown files for generating dashboards. For anything complex, HTML (or a framework that uses HTML-based template syntax like Vue or Svelte) just seems more logical to me. I also haven’t yet been converted over to the notebook style of development with fenced JavaScript blocks.

In short, the speed at which a new project can be set up can make Observable Framework a good solution for prototyping dashboards and interactive websites. Simple deployment options makes it easy to share such prototypes with other stakeholders. For production applications I’m not sure what Observable Framework offers that can’t be built in a more maintainable way with popular, “traditional”, JavaScript frameworks. These can still use Observable Plot, which I do think works nicely and will definitely be using again: you just have to explicitly add it to the project and import it where needed.


Jumping Rivers Logo

Recent Posts

  • Start 2026 Ahead of the Curve: Boost Your Career with Jumping Rivers Training 
  • Should I Use Figma Design for Dashboard Prototyping? 
  • Announcing AI in Production 2026: A New Conference for AI and ML Practitioners 
  • Elevate Your Skills and Boost Your Career – Free Jumping Rivers Webinar on 20th November! 
  • Get Involved in the Data Science Community at our Free Meetups 
  • Polars and Pandas - Working with the Data-Frame 
  • Highlights from Shiny in Production (2025) 
  • Elevate Your Data Skills with Jumping Rivers Training 
  • Creating a Python Package with Poetry for Beginners Part2 
  • What's new for Python in 2025? 

Top Tags

  • R (236) 
  • Rbloggers (182) 
  • Pybloggers (89) 
  • Python (89) 
  • Shiny (63) 
  • Events (26) 
  • Training (23) 
  • Machine Learning (22) 
  • Conferences (20) 
  • Tidyverse (17) 
  • Statistics (14) 
  • Packages (13) 

Authors

  • Amieroh Abrahams 
  • Tim Brock 
  • Aida Gjoka 
  • Shane Halloran 
  • Russ Hyde 
  • Myles Mitchell 
  • Theo Roe 
  • Colin Gillespie 
  • Gigi Kenneth 
  • Osheen MacOscar 
  • Sebastian Mellor 
  • Keith Newman 
  • Pedro Silva 

Keep Updated

Like data science? R? Python? Stan? Then you’ll love the Jumping Rivers newsletter. The perks of being part of the Jumping Rivers family are:

  • Be the first to know about our latest courses and conferences.
  • Get discounts on the latest courses.
  • Read news on the latest techniques with the Jumping Rivers blog.

We keep your data secure and will never share your details. By subscribing, you agree to our privacy policy.

Follow Us

  • GitHub
  • Bluesky
  • LinkedIn
  • YouTube
  • Eventbrite

Find Us

The Catalyst Newcastle Helix Newcastle, NE4 5TG
Get directions

Contact Us

  • hello@jumpingrivers.com
  • + 44(0) 191 432 4340

Newsletter

Sign up

Events

  • North East Data Scientists Meetup
  • Leeds Data Science Meetup
  • Shiny in Production
British Assessment Bureau, UKAS Certified logo for ISO 9001 - Quality management British Assessment Bureau, UKAS Certified logo for ISO 27001 - Information security management Cyber Essentials Certified Plus badge
  • Privacy Notice
  • |
  • Booking Terms

©2016 - present. Jumping Rivers Ltd