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. Blanks are written as runs of underscores (______).
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 * 7Try 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 derived from the question text, so editing a question’s wording resets its saved answers — pass id = "some-stable-slug" to question() if you want to reword a question without losing readers’ progress.
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")
)
)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 orquarto::quarto_render().
A learnr-style progressive, section-by-section theme is on the roadmap.