# install.packages("remotes")
remotes::install_github("your-org/shinyds")shinyds provides R wrappers for Designsystemet, the Norwegian government’s design system. It lets you build Shiny applications that follow Norwegian public sector design guidelines using familiar ds_* functions.
Components come from two upstream sources:
ds-* CSS classes (ds_button(), ds_alert(), ds_input(), …)ds_tabs(), ds_pagination(), ds_suggestion())# install.packages("remotes")
remotes::install_github("your-org/shinyds")Every app needs use_designsystemet() in the UI. It loads the CSS and JavaScript bundles and activates the design token colour scheme.
library(shiny)
library(bslib)
library(shinyds)
ui <- bslib::page_fluid(
use_designsystemet(),
ds_heading("Hello Designsystemet!", level = 1),
ds_paragraph("A Shiny app using Norwegian government design components."),
ds_field(
ds_label("Your name", `for` = "name"),
ds_input("name", placeholder = "Enter your name")
),
ds_button("Submit", inputId = "submit", variant = "primary"),
verbatimTextOutput("out")
)
server <- function(input, output, session) {
output$out <- renderPrint(list(name = input$name, clicks = input$submit))
}
shinyApp(ui, server)bslib::page_fluid() is the recommended page function — it provides Bootstrap 5 and works cleanly alongside Designsystemet’s own CSS tokens.
# label first, then inputId
ds_button("Click me", inputId = "btn1", variant = "primary")
ds_button("Secondary", inputId = "btn2", variant = "secondary")
ds_button("Tertiary", inputId = "btn3", variant = "tertiary")
# sizes
ds_button("Small", size = "sm")
ds_button("Medium", size = "md")
ds_button("Large", size = "lg")
# states
ds_button("Disabled", variant = "primary", disabled = TRUE)
ds_button("Loading", variant = "secondary", loading = TRUE)input$btn1 starts at 0 and increments by 1 on each click.
ds_field(
ds_label("Email", `for` = "email"),
ds_input("email", type = "email", placeholder = "you@example.com"),
ds_validation_message("Must be a valid email address", variant = "error")
)
ds_field(
ds_label("Message", `for` = "msg"),
ds_textarea("msg", placeholder = "Type here…", rows = 4)
)Wrap ds_input() / ds_textarea() in ds_field() with a ds_label() — this is the standard Designsystemet pattern and ensures correct label association and spacing.
ds_checkbox("agree", label = "I accept the terms")
# Radio buttons — group them with a shared name
ds_radio("opt_a", label = "Option A", value = "a", name = "opts", checked = TRUE)
ds_radio("opt_b", label = "Option B", value = "b", name = "opts")input$agree returns TRUE / FALSE. Radio inputs are individual elements; read any one of the group’s inputId values to get the selected value.
ds_field(
ds_label("Country", `for` = "country"),
ds_select("country",
choices = c("Norway" = "no", "Sweden" = "se", "Denmark" = "dk"),
selected = "no"
)
)input$country returns the selected option’s value string.
Group related controls under a shared legend:
ds_fieldset(
legend = "Notification preferences",
ds_checkbox("notif_email", label = "Email"),
ds_checkbox("notif_sms", label = "SMS"),
ds_checkbox("notif_push", label = "Push")
)Note:
ds_fieldset()is backed by a behaviour-only JavaScript module. See the Reactivity Patterns vignette if you need to react to fieldset-level events.
# Search input — input$q returns the current text
ds_field(
ds_label("Search", `for` = "q"),
ds_search("q", placeholder = "Search…")
)
# Autocomplete suggestion — input$fruit returns the selected value
ds_field(
ds_label("Fruit", `for` = "fruit"),
ds_suggestion("fruit",
choices = c("Apple", "Banana", "Cherry"),
placeholder = "Start typing…"
)
)ds_suggestion() includes built-in keyboard navigation and filtering. If you need a fully custom autocomplete — for example, with server-side filtering or custom option rendering — use ds_combobox() instead. It provides the CSS container only; keyboard navigation and dropdown behaviour are the caller’s responsibility.
Use ds_validation_message() on individual fields to show inline errors, and ds_error_summary() at the top of the form to collect all errors in one place. Render both conditionally from the server:
ui <- bslib::page_fluid(
use_designsystemet(),
uiOutput("error_summary"),
ds_field(
ds_label("Email", `for` = "email"),
ds_input("email", placeholder = "you@example.com"),
uiOutput("email_error")
),
ds_button("Submit", inputId = "submit", variant = "primary")
)
server <- function(input, output, session) {
observeEvent(input$submit, {
errors <- list()
if (!nzchar(trimws(input$email %||% ""))) {
errors$email <- "Email is required"
} else if (!grepl("@", input$email, fixed = TRUE)) {
errors$email <- "Must be a valid email address"
}
output$email_error <- renderUI({
if (!is.null(errors$email))
ds_validation_message(errors$email, variant = "error")
})
output$error_summary <- renderUI({
if (length(errors) > 0)
ds_error_summary(
heading = "Please fix the following errors",
tags$li(ds_link(errors$email, href = "#email"))
)
})
})
}Link each item in ds_error_summary() to the corresponding field’s id so keyboard users can jump directly to the problem field.
# Headings — level sets the HTML element (h1–h6), size sets the visual token
ds_heading("Page Title", level = 1, size = "2xl")
ds_heading("Section Title", level = 2, size = "lg")
ds_heading("Card Title", level = 3, size = "md")
ds_paragraph("Body copy.", size = "md")
ds_paragraph("Small caption.", size = "sm")
ds_link("Designsystemet", href = "https://designsystemet.no")
ds_list(
ds_list_item("First"),
ds_list_item("Second"),
ds_list_item("Third"),
ordered = TRUE
)ds_card(
ds_card_block(
ds_heading("Card Title", level = 3, size = "sm"),
ds_paragraph("Card content.")
)
)
ds_card(variant = "tinted",
ds_card_block("Highlighted content")
)ds_table(
ds_thead(
ds_tr(ds_th("Name"), ds_th("Role"), ds_th("Status"))
),
ds_tbody(
ds_tr(ds_td("Alice"), ds_td("Developer"),
ds_td(ds_tag("Active", color = "success"))),
ds_tr(ds_td("Bob"), ds_td("Designer"),
ds_td(ds_tag("Active", color = "success")))
)
)ds_tabs("my_tabs",
ds_tablist(
ds_tab("Overview", value = "overview", selected = TRUE),
ds_tab("Details", value = "details")
),
ds_tabpanel(value = "overview",
ds_paragraph("Overview content.")
),
ds_tabpanel(value = "details",
ds_paragraph("Details content.")
)
)
# In server: input$my_tabs returns the selected tab value stringds_pagination("pager", current = 1, total = 10)
# In server: input$pager returns the current page number (integer)Show the current location within a multi-level structure:
ds_breadcrumbs(
tags$ol(
tags$li(ds_link("Home", href = "/")),
tags$li(ds_link("Reports", href = "/reports")),
tags$li(tags$span("Annual summary")) # current page — plain text, no link
)
)The last item should be plain text rather than a link, since it represents the current page.
Place a skip link at the very top of the page so keyboard users can jump past navigation directly to the main content:
ui <- bslib::page_fluid(
use_designsystemet(),
ds_skip_link("Skip to main content", href = "#main"),
# ... navigation ...
tags$main(id = "main",
# ... page content ...
)
)ds_skip_link() renders as a visually hidden link that becomes visible when it receives keyboard focus. It should be the first focusable element on the page.
ds_alert("Informational message.", variant = "info")
ds_alert("Operation succeeded.", variant = "success")
ds_alert("Review before saving.", variant = "warning")
ds_alert("Something went wrong.", variant = "danger")
ds_spinner(title = "Loading…", size = "md")
ds_skeleton(variant = "text", width = "100%")
ds_skeleton(variant = "circle", width = "48px", height = "48px")
ds_skeleton(variant = "rectangle", width = "200px", height = "80px")
ds_badge_position(
ds_button("Inbox", variant = "secondary"),
ds_badge(count = 4, color = "danger")
)Display user initials or a profile image:
# Initials
ds_avatar("AB", size = "sm")
ds_avatar("CD", size = "md")
ds_avatar("EF", size = "lg")
# Image
ds_avatar(
tags$img(src = "profile.jpg", alt = "Alice B."),
size = "lg"
)
# Group several avatars in a stack
ds_avatar_stack(
ds_avatar("AB"),
ds_avatar("CD"),
ds_avatar("EF"),
ds_avatar("GH")
)A toggleable filter chip with an aria-pressed state:
ds_chip("React", selected = TRUE)
ds_chip("Vue")
ds_chip("Angular")
ds_chip("Svelte")ds_chip() renders as a <button>. To make chips reactive, attach a click listener and call Shiny.setInputValue() — the same pattern used for ds_toggle_group() in the Reactivity Patterns vignette applies here.
ds_tooltip() wraps any element and attaches a tooltip via data-tooltip attributes. Unlike other components it does not create a new element — it modifies the element you pass in:
ds_tooltip(
ds_button("Delete", variant = "danger"),
text = "Permanently removes the record",
placement = "top" # "top", "bottom", "left", "right"
)
ds_tooltip(
ds_badge(count = 12),
text = "Unread notifications"
)Several inputs support programmatic updates:
server <- function(input, output, session) {
observeEvent(input$reset, {
update_ds_input(session, "name", value = "")
update_ds_checkbox(session, "agree", value = FALSE)
update_ds_select(session, "country", value = "no")
update_ds_tabs(session, "my_tabs", selected = "overview")
update_ds_pagination(session, "pager", current = 1)
})
}Because shinyds components are plain HTML tags, they compose naturally with bslib layout functions:
ui <- bslib::page_fluid(
use_designsystemet(),
bslib::layout_sidebar(
sidebar = bslib::sidebar(
title = "Controls",
position = "right",
width = 280,
verbatimTextOutput("values")
),
# Main content
ds_tabs("tabs",
ds_tablist(
ds_tab("Form", value = "form", selected = TRUE),
ds_tab("Result", value = "result")
),
ds_tabpanel(value = "form",
ds_field(ds_label("Name", `for` = "nm"), ds_input("nm")),
ds_button("Submit", inputId = "go")
),
ds_tabpanel(value = "result",
verbatimTextOutput("out")
)
)
)
)Use bslib::layout_columns() for equal-width column grids, and bslib::layout_column_wrap(width = "220px") for auto-fitting responsive grids.
# Minimal form example
shiny::runApp(system.file("examples/basic", package = "shinyds"))
# Data visualisation with tabs and layout_sidebar
shiny::runApp(system.file("examples/faithful", package = "shinyds"))
# Full component reference
shiny::runApp(system.file("examples/showcase", package = "shinyds"))vignette("reactivity-patterns", package = "shinyds")