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 Code (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.

A graded exercise needs its solution as a solution: true code cell (not the .solution fenced div used in section 3) — only the cell form emits the machine-readable copy of the code that gradethis::grade_this_code() compares against. The cell still renders a “Show solution” button for the reader, exactly like the div does.

6 * 7
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 as their 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). Once an image is pasted it is the whole answer: the text box disappears and nothing typed is saved alongside it. “Remove image” brings the text box back.

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 format your platform’s clipboard actually holds — it varies, so a screenshot isn’t guaranteed to be a PNG — allow_image accepts it: PNG, JPEG, GIF, WebP, or BMP. Before saving, it scales the image to at most 1600 pixels wide and stores it as a compact WebP (JPEG in Safari), so a page of screenshots doesn’t fill the browser’s storage. If a save ever fails, the page shows a warning and the answer stays unsubmitted (see the question() help page for the details).

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 carries browser info, a per-device id, and a time field: the moment the reader clicked “Download”, lightly obfuscated so the raw timestamp isn’t readable or hand-editable in the file. Recover it with learnr2::submission_time(path). Nothing else is hashed or signed — with no server involved there is no way to prevent a reader editing their answers; this just keeps the submission time honest at a glance:

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

9. Layout options

Nothing in this section is a widget. It describes the page-level choices an author has, all made in the YAML header or with one function call, and what each one does for the reader.

Table of contents

Every tutorial scaffolded by create_tutorial() has toc: true in its YAML header, which gives the page Quarto’s table-of-contents sidebar: the list of sections on the right of this page. Set toc: false to drop it.

The sidebar is a map, not a shortcut. Sections reveal one at a time behind Continue buttons whether or not there is a TOC; the TOC adds a view of the whole tutorial’s shape and of how far the reader has come. On a phone-width screen Quarto hides the sidebar on its own, so a toc: true tutorial behaves like a toc: false one there, with no author action.

Submitting before continuing

By default, the Continue button at the end of a section stays greyed out, with a note under it, until every question and student-info form above it has been submitted. A reader can type anything, but must submit something before moving on, and a type = "reflection" question locks once submitted. Together those two rules let a tutorial show its own answer immediately after the reader’s, which is what we always want, without inviting anyone to paste our answer back into their box: by the time they see ours, theirs is in and cannot be changed.

This tour is reference material, so it switches the gate off with the chunk below (shown here; hidden in a real tutorial with echo: false). That is why you have been able to click past every question on this page without answering it. Leave this chunk out of a tutorial of your own and the gate is on.

learnr2::tutorial_options(require_submission = FALSE)

Skipping ahead

By default the TOC cannot be used to read ahead. Look at the sidebar now: the entries below this section are dimmed, a click on one does nothing, and each lights up only when its section is unlocked. A reader can see what is coming and where they are, but cannot open the whole tutorial at once.

If you want readers to roam freely — reference material, or a tour like this one — turn on skipping too, in that same tutorial_options() call:

learnr2::tutorial_options(allow_skip = TRUE, require_submission = FALSE)

Then clicking any TOC entry unlocks every section up to that one and jumps there, like a classic learnr tutorial with allow_skip: yes. This page leaves skipping at its default so you can see the dimming in action. Without a TOC the setting has no effect, since there is nothing to click.

Pauses without a heading

Not every Continue stop needs a title. A line holding only ###, with nothing after it, is a pause: the content below it stays hidden until the reader clicks a button that says just Continue, and no heading is shown. The tutorial.helpers house style puts two of these inside every exercise, one before the author’s own answer and one before the knowledge drop, so the reader compares before they read on. Here is one now.

You just clicked through a pause. In the .qmd it is three hash marks on a line by themselves; in the sidebar it has no entry.

Start Over

Every answer, pasted image, and exercise edit on a page is saved in the reader’s browser, so reloading never loses anything. The way to wipe it all is the Start Over button, which learnr2 adds to every tutorial automatically — nothing to write in the .qmd. It asks for confirmation, then clears this page’s saved state (but not the per-device id that download_answers_button() records) and reloads.

Where it appears follows the layout. With a visible sidebar it sits at the bottom of the table of contents, as it does on this page. Without one — toc: false, or a screen too narrow for the sidebar — it moves to the top of the tutorial, directly under the title, where it stays visible no matter how far the reader has progressed. Either way the reader always has exactly one.

Where to go next

  • Use learnr2::create_tutorial("my-tutorial", dir = ".") to scaffold your own.
  • Edit the .qmd, then run learnr2::check_tutorial() on its directory to catch the common authoring mistakes, and learnr2::render_tutorials() (or plain Quarto) to build it.
  • Preview the result with learnr2::run_tutorial(), which renders, serves and opens it in your browser.