Markdown-Based Programmable Surveys Using 'Quarto' and 'shiny'
Overview
Generate programmable surveys using markdown and R code chunks. Surveys are composed of two files: a survey.qmd 'Quarto' file defining the survey content (pages, questions, etc), and an app.R file defining a 'shiny' app with global settings (libraries, database configuration, etc.) and server configuration options (e.g., conditional skipping / display, etc.). Survey data collected from respondents is stored in a 'PostgreSQL' database. Features include controls for conditional skip logic (skip to a page based on an answer to a question), conditional display logic (display a question based on an answer to a question), a customizable progress bar, and a wide variety of question types, including multiple choice (single choice and multiple choices), select, text, numeric, multiple choice buttons, text area, and dates. Because the surveys render into a 'shiny' app, designers can also leverage the reactive capabilities of 'shiny' to create dynamic and interactive surveys.
- New feature:
mc_imageandmc_multiple_imagequestion types — multiple choice where each option is a clickable image card (single selection withmc_image, multiple withmc_multiple_image). Pass a newimageargument tosd_question(): a vector of image paths or URLs, one per option, in the same order asoption(paths resolve against the survey'simages/wwwfolder, e.g."images/cat.png", or use full URLs). Captions are optional and controlled by theoptionnames: named options (e.g.c('Cat' = 'cat')) show the names as text captions beneath each image, while an unnamed option vector (e.g.c('cat', 'dog')) shows the images only with no captions. (This differs from the other multiple-choice types, where an unnamed vector uses the values as visible labels.) Values are stored exactly likemc/mc_multiple(single value, or pipe-joined for multiple), so storage, restoration, and required-question logic all work the same. - New feature:
matrix_multiplequestion type — a matrix where each row allows multiple selections (checkboxes), complementing the single-selectionmatrixtype (radio buttons). Usage is identical tomatrix(samerowandoptionparameters); each row becomes a<question_id>_<row_id>sub-question stored as its own column, with multiple selections pipe-joined (e.g.,"fast|cheap"). Row shuffling, required-question handling, and session restoration work the same as formatrix. - Enhancement: Sliders have been restyled.
sliderandslider_numericquestions now get a slim, theme-colored track with a clean circular handle and value bubble (previously ionRangeSlider shipped completely unstyled). The dense minor tick marks between positions are hidden; only the labeled major tick marks at the main breaks are shown. - Enhancement:
sd_reactive()no longer warns when its expression fails simply because the questions it references are not answered yet (e.g., arithmetic on a blank value at session start). Question references (input$x,all_data$x,sd_value(),sd_values()) are extracted from the expression; if any referenced value is still blank when an error occurs, the error is treated as the expected "not answered yet" state and""is stored quietly. Genuine errors (all referenced values filled in, or no detectable references) still warn. For this detection to work, callsd_value()*inside* thesd_reactive()expression rather than capturing values in local variables beforehand. - Bug fix: Navigating back to and then forward again past an answered matrix (or
matrix_multiple) question no longer crashes the survey. The matrix parent question has no input of its own, so its stored value can beNA; the restoration guard now handlesNAsafely instead of erroring. - Bug fix: Multi-value answers (
mc_multiple,mc_multiple_buttons,daterange, and rangeslider_numeric) are now correctly restored when a respondent navigates back to a page or refreshes. These answers are stored pipe-joined (e.g."red|blue"), but the restoration logic was splitting on commas, so previously the inputs came back blank. They now split on the pipe to match how the values are stored. - Bug fix:
mode: preview(andmode: local) now guarantee that the database is never used, even when a valid connection is passed tosd_server(). Previously, ifsd_db_connect()succeeded whilemode: previewwas set, responses were silently written to the database while the banner claimed they were going topreview_data.csv. The connection is still created and tested bysd_db_connect()(so credentials are verified during preview), but all reads and writes go to the local CSV. When a connection exists but is ignored, the preview banner now reads "Database connected but not used" and a console message explains how to switch to database mode.sd_db_connect()no longer claims "Running in database mode" on connection (it cannot know the survey mode); it simply reports that the connection succeeded.sd_server()now always prints the operating mode on the console at session start for every mode: preview/local (with or without an ignored connection), database with a working connection, and a warning when mode isdatabasebut no connection is available (responses not saved). This also applies tosd_store_value(), which previously could read session-persistence values from the database in preview mode via the auto-detecteddbobject. - Bug fix: Restoring a cookied session in
preview/localmode crashed the app withobject 'local_csv_file' not found(a respondent refreshing the page in these modes hit a grey screen and a new session row). The local CSV filename is now set beforehandle_sessions()runs. The bug was introduced with themodesetting in v1.2.0 and only affected cookie restoration without a database connection; database mode was unaffected. - Bug fix: A warm
_survey/cache is now invalidated when the installed surveydown package is newer than the cached render. Previously, upgrading the package did not trigger a re-render, so surveys kept serving the previous package version's embedded JavaScript and CSS untilsurvey.qmdwas next edited. - Efficiency: Consolidated the per-question observers in
sd_server(). Two of the three observer sets created for every question (*_manual_rangeand*_autosave_timestamp) were dead code — the client-side JavaScript that emitted those inputs was removed in earlier refactors — so each question now registers one observer instead of three. The*_value,*_label_option, and*_label_questionoutputs are registered once per question (reading reactively fromall_data) instead of being re-registered on every input event; as a side benefit,sd_output(id, type = "value")now shows values restored from a resumed session before any new interaction. - Efficiency: In preview/local mode, the response CSV (
preview_data.csv/local_data.csv) is now held in an in-process cache instead of being re-read from disk on every save and every lookup. Local modes run in a single R process, so all sessions share one copy per file; entries are keyed by absolute path and invalidated if the file changes on disk (external edits or deletion are picked up). Saves still write the complete file to disk every time, so no data is lost on a crash. Also removed a duplicate definition ofget_local_data()that existed in bothserver.Randutil.R. Database mode is unaffected. - Efficiency: App startup with a warm
_survey/cache no longer runsquarto::quarto_inspect()(a 1-3s subprocess) or rewritessettings.yml. When nothing changed,sd_ui()reads the progress bar and footer settings from the cached_survey/settings.yml; previously the unconditional settings rewrite also bumped its mtime, forcingrun_config()to re-parse all pages and questions on every startup. This mainly speeds up cold starts of deployed apps (new workers, container restarts). The full inspect path still runs wheneversurvey.qmd, any YAML file, orapp.Rchanges, or if the cached settings are missing/unreadable.
Show all changes in 1.3.0Hide the rest of the changes in 1.3.0
- Efficiency / bug fix: The per-question inline JavaScript for
mc_buttons,mc_multiple_buttons, andnumericquestions has been replaced with delegated handlers ininteraction.js, registered once per page load. Previously each question embedded its own script; for reactive questions (re-rendered via the server), themc_buttons/mc_multiple_buttonsscripts re-registered a new document-level click handler on every re-render, so handlers accumulated over a session. The numeric input's validation, spinner UI, and paste handling now also live in the shared file (the spinner is injected on Shiny'sshiny:boundevent, so it works for reactive questions too), shrinking the rendered HTML. Surveys with a stale_survey/cache keep working (the old inline scripts remain functional alongside the new handlers until the next re-render). - Efficiency: The cookie-sync observer (which mirrors current-page answers, page history, and shuffle orders into the browser cookie) is now debounced by 500ms. Previously it re-serialized all current-page answers and messaged the client on every input change — including every keystroke in text questions. Now a single cookie update is sent shortly after the respondent pauses.
- Efficiency: Parsed
_survey/settings.ymlcontent (settings and system messages) is now cached per Shiny session insession$userData, so repeated calls (e.g., per reactive question or persd_store_value()) no longer re-read and re-parse the file from disk. Calls made outside a Shiny session (e.g., during Quarto rendering ofsurvey.qmd) still read directly from disk. - Efficiency: The
show_ifobserver no longer depends on every input viareactiveValuesToList(input), which re-evaluated allshow_ifconditions on every keystroke of any input. It now depends only on the question values actually referenced in the conditions (extracted statically, plus dependencies registered dynamically during condition evaluation). Sinceshow_iftarget containers are baked into each page's HTML as hidden, the rendered page now signals the server when its DOM is inserted (sd_page_rendered) so question visibility is re-applied on every page render. - Internal: Question types are now defined in a single registry (
R/question_types.R). Each type provides arender()(build the UI) andrestore()(re-apply a stored value when navigating back) function plus arequires_optionflag.sd_question()and the page-restoration logic insd_server()dispatch through this registry instead of long if/else chains, so adding a new question type touches one place. No change to existing behavior. - Testing: Added
tests/manual/with chromote-based headless browser tests coveringsd_show_if()(all condition styles, including cross-pageinput$/all_data$conditions),sd_skip_if()(simple and two-question skips, required-question blocking),sd_stop_if()(stop messages, priority over required warnings, per-page applicability), data storage in preview mode (every question type verified end-to-end inpreview_data.csv, including pipe-joined multi-selects, slider label-to-value mapping, matrix sub-columns,sd_store_value(), and session metadata), and cookies/session restoration (page refresh restores the current page and answers, sessions resume with the samesession_idand a single CSV row, and a fresh browser starts a new session). The tests run against bundled apps intests/manual/apps/, so they are self-contained and machine-independent.tests/manual/run-all.Rruns the whole suite (in parallel whenmiraiis installed). Not run bydevtools::test()or R CMD check.
From NEWS.md in the release
Install
Health
- OK13 OK2026-08-04
- NOTE12 OK, 1 NOTE2026-08-01
- OK13 OK2026-06-09
- WARNING12 OK, 1 WARNING2026-06-08
- NOTE12 OK, 1 NOTE2026-06-07
Show 1 earlier changeHide 1 earlier change
- OK14 OK2026-03-10
Documentation
- Help pages
- 38
- Examples that run
- 87%27 run in a plain check (25 of them under a condition) · 4 never run
- Exported functions with examples
- 82%
- Documented parameters
- 99%
- Return-value docs
- 97%
- Help pages citing references
- 0%
Downloads
Repository
- PRs merged (all time)
- 97%
- Median days to close, 50 most recently updated closed issues
- 7
- Median days to close, 50 most recently updated closed PRs
- 0
Stars over time
Forks over time
Releases over time
Issues over time
PRs over time
12 development-tooling and community-health practices detected across 5 families in the upstream repository
Checks run against github.com/surveydown-dev/surveydown on 2026-10-18.
GitHub Pages site last deployed 2026-06-23 · The repository has a site deploy workflow
pkgdown site last built 2026-06-23
Show all practicesHide 1 practice
This repository shows signs of AI coding tools.
- Claude Codefirst seen 2025-06-11Seen in the repository: CLAUDE.mdNamed in .Rbuildignore: .claude, CLAUDE.mdNamed in .gitignore: .claudeCommits: at least 7 crediting it in the message
- Claude Opus 4.62 commits · Mar 2026
- Claude Sonnet 4.62 commits · Jun 2026
- Claude Opus 4.81 commit · Jun 2026
Dependencies
Installing surveydown adds 71 packages (36 with compiled code, 39 MB of source tarballs). Heaviest: bslib 5.6 MB, promises 3.1 MB, sass 2.9 MB.
Code & Tests
People & History
Pick two releases to compare their code metrics.
- 1.3.0Latest
- RR 4.6.0 released · 2026-04-24
- 1.0.12025-12-19 · diff ↗ ·
removed 1 export
sd_create_translationsadded 2 exports
sd_create_messages, sd_nav - 0.14.02025-11-03 · diff ↗
- 0.13.3
- 0.13.02025-08-20 · diff ↗
- 0.12.62025-07-07 · diff ↗
- 0.11.02025-04-17 · diff ↗
- RR 4.5.0 released · 2025-04-11
- 0.10.12025-04-08 · diff ↗ ·
removed 1 export
sd_show_passwordadded 2 exports
sd_page_gadget, sd_question_gadget - 0.9.0
- 0.8.0
- 0.7.2
- 0.5.12024-11-27 · diff ↗
- 0.5.0
- 0.4.02024-11-01
Show 1 earlier eventHide 1 earlier event
- RR 4.4.0 released · 2024-04-24
Package metadata
- First published
- 2024-11-01
- Total releases
- 14 / 2 yrs
- License
- MIT + file LICENSE OSI
- Minimum R
- ≥ 4.1.0
- Download size
- 650 KB
Cite
Cite this package
Run in R for the authors' entry:
citation("surveydown")The package provides a citation file, so citation() returns the authors' own entry.
It points to 10.1371/journal.pone.0331002.
Cite the R Observatory
For a number measured here: a download total, a coverage figure, an archival date.
From data release v2026-10-22, which the citation names so these numbers can be found later. More on citing and the projects behind them.