psychTestR comes with a collection of built-in page types, including the following:

  • text_input_page
  • audio_NAFC_page
  • video_NAFC_page
  • dropdown_NAFC_page
  • slider_page
  • final_page

Sometimes you will want to create a new page type that doesn’t obviously fit into any of these categories. The most general way of achieving this is using the page function, which takes the following arguments: ui, admin_ui, label, final, get_answer, save_answer, validate, on_complete, and next_elt. We’ll now discuss these arguments in turn; also see the documentation available at ?page.


The ui argument defines the HTML that is presented to the participant. This HTML should be generated programmatically using the helper functions in the shiny package (which often themselves come from the htmltools package). There’s lots of documentation online for these packages, but we’ll give some conceptual examples here.

Different HTML tags are associated with different functions. These functions can be nested in the same way as HTML.


html <- div(
  id = "my_div",
  h3("Heading 1"),
  p("Here is a paragraph of text.", 
    "A paragraph can contain multiple sentences."),
  h3("Heading 2"),
  p("Here is another paragragh.",
    strong("This sentence is in bold."),
    "This sentence is in the same paragraph but it's not in bold.")

When combined, the functions define an HTML object that will render as HTML code to psychTestR app:

## <div id="my_div">
##   <h3>Heading 1</h3>
##   <p>
##     Here is a paragraph of text.
##     A paragraph can contain multiple sentences.
##   </p>
##   <h3>Heading 2</h3>
##   <p>
##     Here is another paragragh.
##     <strong>This sentence is in bold.</strong>
##     This sentence is in the same paragraph but it's not in bold.
##   </p>
## </div>

This HTML code can incorporate Shiny widgets, such as text input boxes, sliders, etc.

html2 <- div(
  p("Here is a slider input:"),
  sliderInput("slider", NULL, 0, 100, 50)

html2 %>% as.character() %>% cat()
## <div>
##   <p>Here is a slider input:</p>
##   <div class="form-group shiny-input-container">
##     <label class="control-label shiny-label-null" for="slider" id="slider-label"></label>
##     <input class="js-range-slider" id="slider" data-skin="shiny" data-min="0" data-max="100" data-from="50" data-step="1" data-grid="true" data-grid-num="10" data-grid-snap="false" data-prettify-separator="," data-prettify-enabled="true" data-keyboard="true" data-data-type="number"/>
##   </div>
## </div>

The UI can also incorporate Javascript elements:

html3 <- div(
  p("This code sets the variable x to 3."),
  tags$script("var x = 3;")

html3 %>% as.character() %>% cat()
## <div>
##   <p>This code sets the variable x to 3.</p>
##   <script>var x = 3;</script>
## </div>


The admin_ui argument allows you to specify additional UI elements that are only visible to the test administrator. We won’t discuss these here, they’re not necessary for most applications.


This is a textual label for the page; it’s not displayed to the participant, but it’s typically stored in the psychTestR results accumulator.


Set this to TRUE to mark the final page in the test.


This is a function for extracting the participant’s answer from the test page. If NULL (default), no answer is extracted. If a function is provided, it should accept the parameters input and .... For example, the get_answer function for text_input_page is defined as follows:

function(input, ...) input$text_input

The input parameter is equivalent to the input parameter in conventional Shiny apps; in particular, if the UI contains a Shiny input widget with an id of my_id, then the value of this input widget can be accessed with input$my_id.


If TRUE, the answer will be saved in the psychTestR results accumulator; otherwise, the answer won’t be actively retained, but it’ll be available (until overwritten) by calling answer(state) within a code block or similar.


This is an optional function that can be called to validate the participant’s response; if it fails, then the participant is told to try again. See ?page for details.


This is an optional function to be executed upon leaving the page. See ?page for details.


This is almost always TRUE, but see ?page for details.

There’s quite a lot of arguments here to master, but in practice, most effort tends to go into the ui function, and the others are typically quick to fill out. A useful strategy when constructing a new page type is to find a similar page type in psychTestR, copy the source code of the corresponding function, and edit it until it does what you want. For example, see the following code for text_input_page:

#' Text input page
#' Creates a page where the participant puts their
#' answer in a text box.
#' @param label Label for the current page (character scalar).
#' @param prompt Prompt to display (character scalar or Shiny tag object).
#' @param one_line Whether the answer box only has one line of text.

#' @param placeholder Placeholder text for the text box (character scalar).
#' @param button_text Text for the submit button (character scalar).
#' @param width Width of the text box (character scalar, should be valid HTML).
#' @param height Height of the text box (character scalar, should be valid HTML).
#' @inheritParams page
#' @export
text_input_page <- function(label, prompt,
                            one_line = TRUE,
                            save_answer = TRUE,
                            placeholder = NULL,
                            button_text = "Next",
                            width = "300px",
                            height = "100px", # only relevant if one_line == FALSE
                            validate = NULL,
                            on_complete = NULL,
                            admin_ui = NULL) {
  text_input <- if (one_line) {
    shiny::textInput("text_input", label = NULL,
                     placeholder = placeholder,
                     width = width)
  } else {
    shiny::textAreaInput("text_input", label = NULL,
                         placeholder = placeholder,
                         width = width,
                         height = height)
  get_answer <- function(input, ...) input$text_input
  body = shiny::div(
    onload = "document.getElementById('text_input').value = '';",
  ui <- shiny::div(body, trigger_button("next", button_text))
  page(ui = ui, label = label, get_answer = get_answer, save_answer = save_answer,
       validate = validate, on_complete = on_complete, final = FALSE,
       admin_ui = admin_ui)

Though there are quite a few customisable parameters here, the core definition is actually pretty simple.