Hello, learnr2

A guided tour of learnr2 features

What is this?

learnr2 lets you write interactive R tutorials that run entirely in the reader’s web browser using Quarto and WebR. There is no Shiny app and no R server: the code cells below execute right here on this page.

This tutorial is itself a learnr2 tutorial. Read it, edit the cells, and run them to see each feature work.

1. Runnable code cells

Every code cell is a live editor. Press Run (or Ctrl/Cmd + Enter) to execute it. Edit the code and run it again — it is yours to experiment with.

Output works the same as in R, including plots:

2. Non-editable cells

Sometimes you want to show code that runs but should not be edited. Use the edit: false option.

3. Exercises

An exercise is a code cell the reader is expected to complete. Mark a cell with the exercise option and give it a label. The cell starts empty (or with a leading comment, as below) for the reader to fill in.

Add persist: true (as shown above) to save the reader’s edits in the browser’s localStorage, keyed to this page and exercise. Refresh the page after editing the cell above — your changes are still there.

Hints

Add a .hint block tied to the exercise label. Readers can reveal it when stuck.

The sum() function adds numbers, and 1:100 is the sequence 1, 2, …, 100.

Solutions

A .solution block reveals a model answer.

sum(1:100)

4. Setup cells

Exercises often need objects to exist before the reader starts. A cell marked setup: true for a given exercise label runs invisibly beforehand. Here we pre-create a vector the next exercise uses.

mean(scores)

5. Automatic grading

learnr2 reuses ‘quarto-live’’s grading, which is powered by the gradethis package. Add a check cell for an exercise; it runs when the reader presses Submit and compares their work against the solution.

6 * 7

Try submitting a wrong answer (e.g. 6 + 7), then the right one, to see the feedback change.

6. Quiz questions

Not every check needs to run code. learnr2::question() adds a learnr-style quiz question — graded entirely in the reader’s browser with plain JavaScript, no Shiny or R server involved. Questions render wherever the R code below is evaluated (a normal {r} chunk, since this runs once while you author the tutorial, not each time a reader loads the page).

Single choice, with a nudge to try again:

learnr2::question(
  "Which function computes the arithmetic mean in base R?",
  learnr2::answer("mean()", correct = TRUE),
  learnr2::answer("average()"),
  learnr2::answer("avg()"),
  allow_retry = TRUE
)

Multiple choice is inferred automatically from how many answers are marked correct:

learnr2::question(
  "Which of these are valid ways to create a vector in R?",
  learnr2::answer("c(1, 2, 3)", correct = TRUE),
  learnr2::answer("1:3", correct = TRUE),
  learnr2::answer("vector(1, 2, 3)"),
  random_answer_order = TRUE
)

Free-text questions compare the reader’s typed answer against one or more acceptable responses:

learnr2::question(
  "What R package powers grading for learnr2 exercises?",
  learnr2::answer("gradethis", correct = TRUE),
  type = "text",
  allow_retry = TRUE
)

Some free-response questions have a definite right answer that’s too long or variable in phrasing to grade with an exact match. type = "reflection" handles that case: it shows the reader a model answer after they submit their own response, without grading it — and locks their response so it can’t be changed afterward:

learnr2::question(
  "Explain why the code cells on this page don't need a running R server.",
  learnr2::answer(
    "They execute in the reader's own browser via WebR (R compiled to
    WebAssembly), not on a remote server --- so the tutorial keeps working
    even after wherever it was originally hosted goes offline.",
    correct = TRUE
  ),
  type = "reflection"
)

type = "reflection_editable" works the same way, except the reader can keep revising their own response after the model answer is revealed:

learnr2::question(
  "Explain the difference between `type = \"text\"` and `type =
  \"reflection\"`.",
  learnr2::answer(
    "`\"text\"` grades an exact match against one or more accepted answers;
    `\"reflection\"` never grades anything, it just shows a model answer for
    comparison.",
    correct = TRUE
  ),
  type = "reflection_editable"
)

Submitted answers are saved automatically (again via localStorage, keyed by page URL and question) and restored on the reader’s next visit. The default key is the question’s {r} chunk label, so rewording a question is safe — but renaming its chunk resets that question’s saved answers. Pass id = "some-stable-slug" to question() to pin the key regardless of the chunk label.

Group related questions with learnr2::quiz():

learnr2::quiz(
  caption = "Quick check",
  learnr2::question(
    "2 + 2 equals?",
    learnr2::answer("4", correct = TRUE),
    learnr2::answer("22")
  ),
  learnr2::question(
    "Is R free and open source?",
    learnr2::answer("Yes", correct = TRUE),
    learnr2::answer("No")
  )
)
Quick check

7. Copy/paste an image

For exercises where the reader’s work is a plot or other image, add allow_image = TRUE to a "reflection"/"reflection_editable" question and let them paste a screenshot from their clipboard alongside their written response — there is no file-upload button, just press Ctrl+V (or Cmd+V) with the response box focused (or click the box below it and paste there instead).

Try it: edit the exercise below (or leave it as-is), run it, then take a screenshot of the plot it produces — e.g. Cmd+Shift+4 on a Mac, Win+Shift+S on Windows, or your Chromebook’s screenshot tool (Ctrl + Show windows) — and paste it into the question underneath.

learnr2::question(
  "Paste a screenshot of the plot you made in the exercise above.",
  learnr2::answer(
    "A scatterplot of `wt` vs. `mpg`, colored steel blue.",
    correct = TRUE
  ),
  type = "reflection",
  allow_image = TRUE
)

Whatever your OS’s native clipboard format actually is behind the scenes (bitmap, TIFF, etc. — it genuinely varies by platform, so a screenshot isn’t guaranteed to already be a PNG), allow_image accepts it directly — PNG, JPEG, GIF, WebP, or BMP — and re-encodes it as PNG itself before storing it, so what’s actually saved is always PNG no matter which of those your platform handed over (see [question()]’s docs for the exact size limit).

8. Student info and submission

Two more pieces for wrapping a tutorial into something a student can turn in, no server required. In a real tutorial, student_info() normally goes at the very top and download_answers_button() at the very bottom; they’re shown together here just for the demo.

student_info() collects identifying information (name, email, an optional ID by default). It’s deliberately not a quiz question — there is nothing to grade and no model answer, just a plain form with its own Submit button. It also auto-saves as the reader types, so nothing is lost even if they never click Submit:

learnr2::student_info()

download_answers_button() gathers every question() and student_info() answer already saved on the page into one readable JSON file and downloads it — so a reader can save their work and turn it in (attach to an email, upload to an LMS), all without a server to submit to. The download also includes a timestamp, browser info, a per-device id, and a SHA-256 integrity hash over the content — run learnr2::verify_submission(path) on a downloaded file to check it hasn’t been edited since it was downloaded (e.g. a wrong answer quietly changed to a right one). This is tamper-evidence, not proof of identity: with no server involved, a technical reader could reproduce the hash themselves — it’s a deterrent against casually editing the file, not real cryptographic security:

learnr2::download_answers_button(filename_prefix = "hello-learnr2")

Where to go next

  • Use learnr2::create_tutorial("my-tutorial") to scaffold your own.
  • Edit the .qmd, then render with Quarto or quarto::quarto_render().

A learnr-style progressive, section-by-section theme is on the roadmap.