Tabs organize content into separate views where only one view is visible at a time.
Basic Usage
Use TabContext.static(), TabList.static(),
and TabPanel() to create tabs. These are custom wrappers
around @mui/lab
tab components that manage tab state internally with
React.useState, so they work everywhere – in Shiny apps,
Quarto documents, and plain R Markdown – with no server logic
required.
library(muiMaterial)
TabContext.static(
value = "one",
Box(
sx = list(borderBottom = 1, borderColor = "divider"),
TabList.static(
Tab(label = "Item One", value = "one"),
Tab(label = "Item Two", value = "two"),
Tab(label = "Item Three", value = "three")
)
),
TabPanel(value = "one", "Content 1"),
TabPanel(value = "two", "Content 2"),
TabPanel(value = "three", "Content 3")
)The value argument in TabContext.static()
sets the initially selected tab. Each Tab and
TabPanel.static share a matching value string
to link headers to content.
Quarto usage
In a Quarto document (.qmd), use the
.static variants inside an R code chunk. No server function
or runtime: shiny is needed.
---
title: "Tabs Example"
format: html
---
``` r
library(muiMaterial)
TabContext.static(
value = "one",
Box(
sx = list(borderBottom = 1, borderColor = "divider"),
TabList.static(
Tab(label = "Item One", value = "one"),
Tab(label = "Item Two", value = "two"),
Tab(label = "Item Three", value = "three")
)
),
TabPanel(value = "one", Typography("First tab content")),
TabPanel(value = "two", Typography("Second tab content")),
TabPanel(value = "three", Typography("Third tab content"))
)
```
```{=html}
<div class="react-container" data-react-id="kdcrprizswzfrkbqpkwn">
<script class="react-data" type="application/json">{"type":"element","module":"@/muiMaterial","name":"MuiStaticTabContext","props":{"type":"object","value":{"value":{"type":"raw","value":"one"},"children":{"type":"array","value":[{"type":"element","module":"@mui/material","name":"Box","props":{"type":"object","value":{"sx":{"type":"raw","value":{"borderBottom":1,"borderColor":"divider"}},"children":{"type":"element","module":"@/muiMaterial","name":"MuiStaticTabList","props":{"type":"object","value":{"children":{"type":"array","value":[{"type":"element","module":"@mui/material","name":"Tab","props":{"type":"raw","value":{"label":"Item One","value":"one"}}},{"type":"element","module":"@mui/material","name":"Tab","props":{"type":"raw","value":{"label":"Item Two","value":"two"}}},{"type":"element","module":"@mui/material","name":"Tab","props":{"type":"raw","value":{"label":"Item Three","value":"three"}}}]}}}}}}},{"type":"element","module":"@mui/lab","name":"TabPanel","props":{"type":"object","value":{"value":{"type":"raw","value":"one"},"children":{"type":"element","module":"@mui/material","name":"Typography","props":{"type":"raw","value":{"children":"First tab content"}}}}}},{"type":"element","module":"@mui/lab","name":"TabPanel","props":{"type":"object","value":{"value":{"type":"raw","value":"two"},"children":{"type":"element","module":"@mui/material","name":"Typography","props":{"type":"raw","value":{"children":"Second tab content"}}}}}},{"type":"element","module":"@mui/lab","name":"TabPanel","props":{"type":"object","value":{"value":{"type":"raw","value":"three"},"children":{"type":"element","module":"@mui/material","name":"Typography","props":{"type":"raw","value":{"children":"Third tab content"}}}}}}]}}}}</script>
<script>jsmodule['@/shiny.react'].findAndRenderReactData('kdcrprizswzfrkbqpkwn')</script>
</div>
```
Why @mui/lab?
MUI provides tab components in two packages:
-
@mui/material:TabsandTab– base components that handle the tab header and selection state. Always controlled: requires avalueprop and anonChangehandler. -
@mui/lab:TabContext,TabList, andTabPanel– higher-level wrappers that add automatic coordination between tab headers and content panels.
The .static variants wrap @mui/lab
components and add internal React.useState management,
which maps naturally to R’s declarative UI model – you declare the tabs
and their content, and the switching is handled automatically.
Advanced: .shinyInput Variants
The .shinyInput variants
(TabContext.shinyInput(),
TabList.shinyInput(), TabPanel.shinyInput())
expose the selected tab to the Shiny server as a reactive input. This is
useful when:
- Server-side reactions to tab changes: You need the server to know which tab is active – for example, to lazy-load expensive content, trigger data fetches, or log analytics only when a specific tab is viewed.
-
Programmatic tab switching: The server can change
the active tab via
updateTabContext.shinyInput()in response to other events (e.g., completing a form step, receiving a notification). -
Conditional server logic: You want
observeEvent(input$tabList, ...)to run code when the user switches tabs, such as refreshing a plot or resetting a form.
library(shiny)
library(muiMaterial)
ui <- muiMaterialPage(
TabContext.shinyInput(
inputId = "tabContext",
value = "one",
Box(
sx = list(borderBottom = 1, borderColor = "divider"),
TabList.shinyInput(
inputId = "tabList",
value = "one",
Tab(label = "Item One", value = "one"),
Tab(label = "Item Two", value = "two"),
Tab(label = "Item Three", value = "three")
)
),
TabPanel.shinyInput(inputId = "panel1", value = "one", "Content 1"),
TabPanel.shinyInput(inputId = "panel2", value = "two", "Content 2"),
TabPanel.shinyInput(inputId = "panel3", value = "three", "Content 3")
)
)
server <- function(input, output, session) {
# Bridge TabList selection to TabContext
observe({
updateTabContext.shinyInput(
inputId = "tabContext",
value = input$tabList
)
})
# React to tab changes on the server
observeEvent(input$tabList, {
message("User switched to tab: ", input$tabList)
})
}
shinyApp(ui, server)Because TabContext and TabList are separate
Shiny inputs, clicking a tab updates TabList’s value but
does not automatically propagate to TabContext. The
observe() block bridges them: it watches
input$tabList and calls
updateTabContext.shinyInput() to match, which controls
which TabPanel is visible.
Note that TabPanel.shinyInput() requires an
inputId even though it never produces a value for the
server. This is because .shinyInput() routes through the
package’s JavaScript bundle where it shares the same React tree as
TabContext. The plain TabPanel() would resolve
in a separate module context, breaking the React context connection.
