Get started

In a minute

mascot puts a small character, drawn in code, into your Shiny app or Quarto page. The character shows a mood with a status line beside it and wiggles into its new face when the mood changes. You control it from R with plain arguments; no AI service is involved. The package is built for good-practice (GxP) work: a mascot sits beside your ordinary field errors and results, and does not replace them.

What you get:

Install the released version of mascot from CRAN:

install.packages("mascot")

Or install the development version from GitHub with pak:

# install.packages("pak")
pak::pak("insightsurge/mascot")

Try it without writing code

The fastest way to see what mascot can do is the studio, a small app that runs on your computer and needs no internet. It is experimental, so its looks may change, and it uses the optional bslib and knitr packages.

mascot_studio()
mascot_studio(view = "theme")

Your first app

Suppose you have a batch review page where Tessa, the default character, waits in the corner until someone clicks Finish review, then smiles and confirms that every row passed. Put this code in an app.R file and run it from an interactive R session:

library(shiny)
library(mascot)

ui <- fluidPage(
  h1("Batch review"),
  actionButton("finish", "Finish review"),
  mascot("helper", status = "Ready to review")
)

server <- function(input, output, session) {
  observeEvent(input$finish, {
    update_mascot(
      "helper",
      mood = "happy",
      status = "Review complete",
      bubble = mascot_bubble("All rows checked.", duration = 5)
    )
  })
}

shinyApp(ui, server)

How each line works:

Open an example

You can try the bundled Shiny apps before writing your own code.

mascot_examples()
#>         name                        title
#> 1     studio                Mascot studio
#> 2 form-check                 Check a form
#> 3  long-task           Show task progress
#> 4    modules              Work in modules
#> 5      theme               Theme a mascot
#> 6      golem                Work in golem
#> 7      rhino Use a rhino-shaped structure
#>                                                                                             description
#> 1                            Every function and argument, live, with explanations and the code to copy.
#> 2                               A worried helper names invalid fields and becomes happy when both pass.
#> 3     Thinking during a slow step, then success or a lasting error bubble. Requires promises and later.
#> 4                                                Two module helpers and a parent-owned floating helper.
#> 5 A palette, your own or your brand's colours, the code to copy and every mood checked. Requires bslib.
#> 6               Function-built UI, golem runtime options and two nested review modules. Requires golem.
#> 7                             Plain Shiny view modules with two nested reviews; no rhino or box needed.
mascot_example("form-check")
mascot_studio()

The big ideas

Moods and status

When a form has missing entries or a report takes 20 seconds to build, update the mood and status line together so people using your app can see what is happening.

A short animation: Knurl is idle, then thinking while a report runs through three steps (reading the data, fitting the model, writing the report), and celebrating when the report is ready. His status text and the matching update_mascot() call change with each step.

mascot_characters()
#>      id  name
#> 1 tessa Tessa
#> 2  sela  Sela
#> 3 knurl Knurl
#> 4  neer  Neer
#>                                                                                        description
#> 1     A calm teal hexagonal maker with a silver ring, a bow and a chest badge that shows her mood.
#> 2            A reassuring lilac Hexonian with time blocks; suits scheduling and resource planning.
#> 3 A single-wheeled repair bot with a clamp arm and screen visor; suits job queues and diagnostics.
#> 4                                                           A blue wave crest with a scallop clip.
#>       family is_default built_in    lifecycle
#> 1  hexonians       TRUE     TRUE       stable
#> 2  hexonians      FALSE     TRUE       stable
#> 3  visorfolk      FALSE     TRUE experimental
#> 4 elementals      FALSE     TRUE experimental
#>                                                                                                                                          palettes
#> 1                                        night_teal, sea_green, forest, spring, peach, coral_navy, desert_coral, rose_soft, lilac_soft, mist_navy
#> 2 lilac_violet, orchid, coral_navy, azure, peach_ink, lilac_soft, mist_navy, desert_clay, forest, apricot_navy, blush_plum, lilac_dusk, sea_green
#> 3                                                                                                  original, orbit, carbon, cobalt_droid, crimson
#> 4                                                                                       original, lagoon, glacier, coral_reef, night_bay, monsoon
mascot_moods("tessa")
#>          name                                          description
#> 1        idle                          At rest; the starting mood.
#> 2    thinking    Working on something, such as a long calculation.
#> 3       happy                                 Something went well.
#> 4     worried Something needs attention, such as an invalid field.
#> 5 celebrating              A big success, such as a finished task.
#>                               status shared fallback
#> 1 Resting, ready for the next piece.   TRUE     <NA>
#> 2     Working out how the parts fit.   TRUE     <NA>
#> 3         That came together nicely.   TRUE     <NA>
#> 4      One part needs a closer look.   TRUE     <NA>
#> 5       Finished, and built to last.   TRUE     <NA>
update_mascot("helper", mood = "worried", status = "2 fields need attention")
update_mascot("helper", mood = "happy", status = mascot_status())

You can read and check a character file with mascot_read_character() or mascot_check_character() (both experimental) and pass the result to mascot_character(), mascot(), update_mascot() or mascot_preview(). Saving and registering characters and palettes are planned for a later release. See Write your own character for details.

Message bubbles

While the status line shows the current state, a speech bubble announces a specific event, such as three skipped rows in an uploaded file.

mascot_bubble("Three rows skipped.")
mascot_bubble("Check the dates in column B.", duration = Inf)
mascot_bubble()
update_mascot(
  "helper",
  mood = "worried",
  status = "Import needs attention",
  bubble = mascot_bubble("Check the dates in column B.", duration = Inf)
)

One-off gestures

When someone saves a filter or switches tabs, a one-off gesture plays a quick nod or wave without changing the character’s mood.

mascot_gestures("tessa")
#>      name                                                       description
#> 1     nod                    A short nod, such as to acknowledge an action.
#> 2    wave A wave, such as a greeting; characters without arms rock instead.
#> 3    rock                                                      Rock gently.
#> 4 shimmer                                 A glint runs once round the ring.
#>   fallback frames
#> 1     <NA>     18
#> 2     rock     30
#> 3     <NA>     18
#> 4     <NA>     45
update_mascot("helper", gesture = mascot_gesture("nod"))

Size and placement

A mascot can float in a window corner as an app-wide helper or sit inline inside a card right next to a form.

mascot_size(180, small_screen = 72)
mascot_placement("bottom-right", minimise = TRUE, drag = "snap")
mascot("card_helper", size = 120, placement = "inline")
mascot("corner_helper", placement = mascot_placement("bottom-right", drag = "snap"))
update_mascot("corner_helper", placement = mascot_placement("top-left"))

Theme and light/dark mode

When your app has its own brand palette or a dark mode switch, you can match the character’s colours through theme.

mascot_palettes("tessa")
#>            name
#> 1    night_teal
#> 2     sea_green
#> 3        forest
#> 4        spring
#> 5         peach
#> 6    coral_navy
#> 7  desert_coral
#> 8     rose_soft
#> 9    lilac_soft
#> 10    mist_navy
#>                                                              description
#> 1                  The character's own colours, with no palette applied.
#> 2           Sea-green face, silver ring, dark navy hair and a coral bow.
#> 3                  Forest-green face, gold ring and bow, deep pine hair.
#> 4        Light spring-green face, gold ring, forest hair and a pink bow.
#> 5           Peach face, amber ring, plum-brown hair and a red-coral bow.
#> 6                 Pale peach face, coral ring, navy hair and a teal bow.
#> 7                 Sand face, coral ring, terracotta hair and a gold bow.
#> 8  Soft rose face, rose-gold ring, berry-brown hair and a pale gold bow.
#> 9    Soft lilac face, mauve ring, dusky violet hair and a pale gold bow.
#> 10       Pale blue-grey face, steel-blue ring, navy hair and a gold bow.
#>       source
#> 1     native
#> 2  character
#> 3  character
#> 4  character
#> 5  character
#> 6  character
#> 7  character
#> 8  character
#> 9  character
#> 10 character
pink_theme <- mascot_theme(
  light = list(body = "#B53D78", rim = "#A94A82"),
  dark = list(body = "#F2A6CF", rim = "#C5B0EB")
)

mascot("helper", theme = pink_theme)
update_mascot("helper", theme = "coral_navy")

Motion

On a busy review dashboard where extra movement would distract people, you can quieten or stop animation while keeping every mood and message working.

mascot_motion_settings(idle = FALSE)
mascot_motion_settings(others = FALSE, gestures = TRUE)
mascot("still_helper", motion = FALSE)
update_mascot("still_helper", motion = mascot_motion_settings(idle = FALSE))

Clicks and menus

A mascot is a plain image by default so keyboard users never land on a dead control, but click turns it into a button or a menu.

mascot(
  "helper",
  click = mascot_popup_menu(c(finish = "Finish review", reset = "Reset helper"))
)

observeEvent(input$helper_menu, {
  if (identical(input$helper_menu, "finish")) {
    update_mascot(
      "helper",
      mood = "happy",
      status = "Review complete",
      bubble = mascot_bubble("All checks finished.", duration = Inf),
      gesture = mascot_gesture("nod")
    )
  }
})

Shiny modules

When you split a larger Shiny app into modules, including golem and rhino apps, each module can own and update its own mascot.

review_ui <- function(id) {
  ns <- shiny::NS(id)
  shiny::tagList(
    shiny::actionButton(ns("finish"), "Finish review"),
    mascot(ns("helper"), placement = "inline")
  )
}

review_server <- function(id) {
  shiny::moduleServer(id, function(input, output, session) {
    shiny::observeEvent(input$finish, {
      update_mascot("helper", mood = "happy", status = "Review complete")
    })
  })
}

Quarto pages

You can also place mascot() in a rendered Quarto document with no Shiny server to show a fixed mood and status line.

Put this in an R chunk of your .qmd file:

#| echo: false
library(mascot)
mascot("helper", mood = "happy", status = "Rendered with Quarto", placement = "inline")

Check a look before using it

Before wiring a character or colour theme into your app, you can inspect every mood and size on one offline page or try controls live in the studio.

page <- mascot_preview(
  theme = mascot_theme(
    light = list(body = "#B53D78", rim = "#A94A82"),
    dark = list(body = "#F2A6CF", rim = "#C5B0EB")
  )
)
attr(page, "contrast")

mascot_studio(view = "theme")

Leave unchanged or reset

When you call update_mascot() from the server, you can leave settings alone, replace them, or reset them to their defaults.

Setting Reset sent to update_mascot() Result
Character character =mascot_character() Switches back to "tessa"
Mood mood =mascot_mood() Returns to "idle"
Size size =mascot_size() Resets to 160 pixels (small_screen = "auto")
Custom status status =mascot_status() Follows the mood again
Bubble bubble =mascot_bubble() Removes the speech bubble
Theme theme =mascot_theme() (or "original") Restores the character’s own colours
Click click =mascot_action() (or mascot_popup_menu()) Makes the mascot a picture again
Placement placement =mascot_placement() Floats in "bottom-right" ("inline" for inline)
Motion motion =mascot_motion_settings() Allows movement again
# Change only the mood; status, bubble and theme stay as they were:
update_mascot("helper", mood = "thinking")

# Reset custom status, clear the bubble, and restore original colours:
update_mascot(
  "helper",
  status = mascot_status(),
  bubble = mascot_bubble(),
  theme = mascot_theme()
)

Where next