Overview
Adding interactivity to an article is a great way to help readers explore the concepts and data you are presenting more deeply. There are three ways to add interactive components to Quarto documents:

Create custom JavaScript visualizations using Observable JS.
Use the Shiny R package to add interactivity to Knitr engine documents.
Incorporate Jupyter Widgets or htmlwidgets (for the Jupyter and Knitr engines, respectively) into your document.

Each of these techniques has distinct benefits and drawbacks in terms of expressiveness, ease of development, and deployment requirements. We’ll touch on these considerations briefly below, then provide links to more in depth documentation for learning more.


Observable JS
Quarto includes native support for Observable JS, a set of enhancements to vanilla JavaScript created by Mike Bostock (also the author of D3). Observable JS is distinguished by its reactive runtime, which is especially well suited for interactive data exploration and analysis.
Here’s an example that provides slider inputs to condition the behavior of a visualization:


viewof talentWeight = Inputs.range([-2, 2], { value: 0.7, step: 0.01, label: "talent weight" })
viewof looksWeight = Inputs.range([-2, 2], { value: 0.7, step: 0.01, label: "looks weight" })
viewof minimum = Inputs.range([-2, 2], { value: 1, step: 0.01, label: "minimum fame" })






















{
  const w = 400
  const h = 400;
  const result = d3.create("svg").attr("width", w).attr("height", h);
  const margin = 20;
  const xScale = d3.scaleLinear().domain([-2, 2]).range([margin, w - margin]);
  const yScale = d3.scaleLinear().domain([-2, 2]).range([h - margin, margin]);
  const points = result
    .append("g")
    .selectAll("circle")
    .data(actors)
    .join(enter => {
       const sel = enter
         .append("circle")
         .attr("r", 3)
         .attr("cx", d => xScale(d.talent))
         .attr("cy", d => yScale(d.looks))
         .attr("fill", d3.lab(50, 40, 20));
       return sel.filter(d => d.fame <= minimum)
         .attr("fill", "rgb(200, 200, 200)")
         .attr("r", 2);
    });
    
  const linearRegression = regression.regressionLinear()
    .x(d => d.talent)
    .y(d => d.looks)
    .domain([-2, 2]);

  const chosenActors = actors
    .filter(d => d.fame > minimum);

  const line = result
    .append("g")
    .append("line")
    .attr("stroke", d3.lab(20, 40, 20))
    .attr("stroke-width", 1.5)
    .datum(linearRegression(chosenActors))
    .attr("x1", d => xScale(d[0][0]))
    .attr("x2", d => xScale(d[1][0]))
    .attr("y1", d => yScale(d[0][1]))
    .attr("y2", d => yScale(d[1][1]));


  const xAxis = d3.axisBottom(xScale).ticks(3);
  result.append("g")
    .attr("transform", `translate(0, ${yScale(0)})`)
    .call(xAxis);

  result.append("text")
    .attr("x", xScale(0.05))
    .attr("y", yScale(2))
    .text("Looks");

  result.append("text")
    .attr("y", yScale(0.1))
    .attr("x", xScale(-2))
    .text("Talent");

  const yAxis = d3.axisLeft(yScale).ticks(3);
  result.append("g")
    .attr("transform", `translate(${xScale(0)}, 0)`)
    .call(yAxis);
  
  return result.node();
}














actors = transpose(points).map(v => ({
  talent: v.x,
  looks: v.y,
  fame: v.x * talentWeight + v.y * looksWeight
}));






transpose = function(df)
{
  const keys = Object.keys(df);
  return df[keys[0]]
    .map((v, i) => Object.fromEntries(keys.map(key => [key, df[key][i] || undefined])))
    .filter(v => Object.values(v).every(e => e !== undefined));
}
regression = require('d3-regression@1');













Observable JS uses some special keywords and a custom runtime to make JavaScript reactive. For example, the “minimum fame” slider in the example above was created with the following code:
viewof minimum = Inputs.range([-2, 2], { 
  value: 1, step: 0.01, 
  label: "minimum fame"
})
It’s then referenced as a normal JavaScript variable in code that creates the plot:
sel.filter(d => d.fame <= minimum)
As the user interacts with the slider, the minimum value is updated and any code that references it is automatically re-executed.
One benefit of using JavaScript for interactive documents is that all the logic and computation is performed on the client (so no server is required for deployment).
To learn more see the articles on Observable JS.


Shiny
The Shiny package provides a flexible, easy to use framework for creating interactive web applications with R. Quarto in turn includes support for embedding Shiny components and applets into documents created with the Knitr engine.
Here’s a live example of Shiny interactive components along with a brief explanation of the code required to create them:







Shiny comes with a variety of built in input widgets. With minimal syntax it is possible to include widgets like the ones shown on the left in your apps:
# Select type of trend to plot
selectInput(inputId = "type", 
            label = strong("Trend index"),
            choices = unique(trend_data$type),
            selected = "Travel")
Displaying outputs is equally hassle-free:
mainPanel(
  plotOutput(outputId = "lineplot", 
             height = "300px"),
)
Build your plots or tables as you normally would in R, and make them reactive with a call to the appropriate render function:
output$lineplot <- renderPlot({
  plot(x = selected_trends()$date, 
       y = selected_trends()$close, 
       type = "l",
       xlab = "Date", 
       ylab = "Trend index")
})



Shiny makes it very straightforward to create interactive documents using only R. Unlike using JavaScript though, you will need to deploy documents that use Shiny to a server.
To learn more see the articles on Using Shiny with Quarto.


Widgets
Jupyter Widgets and htmlwidgets are great ways to incorporate interactivity into your documents if you don’t know JavaScript and prefer to work exclusively in Python or R. They also run entirely client-side so can be deployed within normal static HTML documents.
For example, the following Python code is all that is required to embed a Leaflet map into a Jupyter engine document:

```{python}
from ipyleaflet import Map, Marker, basemaps, basemap_to_tiles
m = Map(
  basemap=basemap_to_tiles(
    basemaps.NASAGIBS.ModisTerraTrueColorCR, "2017-04-08"
  ),
  center=(52.204793, 360.121558),
  zoom=4
)
m.add_layer(Marker(location=(52.204793, 360.121558)))
m
```




To learn more see these articles on using widgets with Quarto:

Jupyter Widgets (Jupyter engine).
htmlwidgets (Knitr engine).



Layout
Once you’ve gotten familiar with using various interactive components see the article on Component Layout to learn how to:

Group inputs into an input panel.
Present multiple outputs in a tabset panel.
Use a full page layout rather than the default article layout.
Add a sidebar panel for inputs in a full page layout.
Create custom panel layouts to arrange outputs into rows and columns.



Observable JS on the RStudio IDE
Observable JS offers full access to NPM libraries, and these tend to use JS features that require the Electron version of the RStudio IDE daily builds. If you plan on using Observable JS in the RStudio IDE, we recommend the Electron daily builds.