Até aqui usamos fluidPage, sidebarLayout e afins sem perguntar o que eles são. A verdade libertadora: toda UI de Shiny é apenas HTML gerado por funções R. Quando você entende isso, deixa de ser refém dos layouts prontos e passa a construir qualquer interface — responsiva, com a estrutura que quiser, funcionando bem do celular ao monitor ultrawide. Este módulo abre a caixa-preta do HTML do Shiny e apresenta o bslib, a forma moderna de montar layouts responsivos.
Entender que a UI do Shiny é HTML produzido por htmltools::tags; dominar o sistema de grid do Bootstrap (linhas e colunas de 12 unidades) e os layouts modernos do bslib (page_sidebar, layout_columns, card, value_box); e gerenciar recursos estáticos (CSS/JS/imagens) na pasta www/.
A UI é HTML: tags e htmltools
Rode isto no console e veja o que sai:
library(shiny)
fluidPage(
h1("Olá"),
p("Um parágrafo")
)
# <div class="container-fluid">
# <h1>Olá</h1>
# <p>Um parágrafo</p>
# </div>
Funções da UI retornam HTML. Cada tag HTML tem um construtor em R via tags$*, e os mais comuns têm atalhos (h1, p, div, a, span...).
tags$div(
class = "painel destaque", # atributo class
id = "meu-painel", # atributo id
tags$h2("Título"),
tags$p("Texto com ", tags$strong("negrito"), " no meio."),
tags$a(href = "https://posit.co", "Link", target = "_blank")
)
Isso significa que você pode gerar HTML programaticamente — um cartão para cada linha de um data frame, por exemplo:
# Gera uma lista de cards a partir de dados (padrão poderosíssimo)
cards <- lapply(seq_len(nrow(produtos)), function(i) {
tags$div(class = "card",
tags$h3(produtos$nome[i]),
tags$p(paste("R$", produtos$preco[i]))
)
})
# 'cards' é uma lista de tags que você injeta na UI com tagList(cards)
htmltools (que o Shiny usa por baixo) fornece tagList() (agrupar tags), HTML() (HTML literal — cuidado com XSS, tema de notificações, validação e segurança), tags$head() (inserir no <head>) e includeCSS/includeScript.
O grid do Bootstrap: 12 colunas
O Shiny usa o Bootstrap, cujo sistema de layout é uma grade de 12 colunas. Você distribui o espaço somando até 12. Isso é a base da responsividade: em telas pequenas, as colunas empilham automaticamente.
fluidRow(
column(width = 4, "Um terço (4/12)"),
column(width = 8, "Dois terços (8/12)")
)
fluidRow(
column(6, plotOutput("g1")),
column(6, plotOutput("g2")) # dois gráficos lado a lado, 50/50
)
O sidebarLayout clássico nada mais é que um grid pré-montado: sidebarPanel ocupa 4 colunas, mainPanel ocupa 8. Saber disso permite quebrar o layout quando o padrão não serve.
bslib: o layout moderno e responsivo
fluidPage e sidebarLayout ainda funcionam, mas o pacote bslib é a forma atual e recomendada de montar UIs: componentes responsivos, temas fáceis (identidade visual) e melhor aparência por padrão. Os blocos principais:
library(bslib)
ui <- page_sidebar(
title = "Dashboard de Vendas",
sidebar = sidebar(
title = "Filtros",
selectInput("regiao", "Região", choices = c("Todas", "N", "S")),
sliderInput("ano", "Ano", 2020, 2024, 2024)
),
# conteúdo principal
layout_columns(
col_widths = c(4, 4, 4), # três colunas iguais
value_box("Receita", "R$ 1,2M", showcase = bsicons::bs_icon("cash")),
value_box("Pedidos", "3.402", showcase = bsicons::bs_icon("cart")),
value_box("Ticket", "R$ 352", showcase = bsicons::bs_icon("receipt"))
),
layout_columns(
col_widths = c(8, 4),
card(card_header("Série temporal"), plotOutput("serie")),
card(card_header("Ranking"), plotOutput("rank"))
)
)
Os quatro componentes que você usará o tempo todo:
page_sidebar— página com barra lateral responsiva (colapsa no celular). Há tambémpage_navbar(abas no topo) epage_fillable(ocupa a tela toda).card— o contêiner visual padrão do dashboard moderno, comcard_header,card_body,card_footer.value_box— o "KPI card" (número grande + ícone) do topo de todo dashboard.layout_columns— grid responsivo declarativo, ondecol_widthsdistribui as 12 colunas.
Responsividade de verdade: col_widths por breakpoint
layout_columns permite larguras diferentes por tamanho de tela usando breakpoints() — a essência do design responsivo (três colunas no desktop, empilhadas no celular).
layout_columns(
col_widths = breakpoints(
sm = c(12, 12, 12), # celular: cada card ocupa a linha inteira (empilha)
md = c(6, 6, 12), # tablet: dois em cima, um embaixo
lg = c(4, 4, 4) # desktop: três lado a lado
),
card("A"), card("B"), card("C")
)
Os breakpoints do Bootstrap (sm, md, lg, xl) são larguras de tela; definir col_widths por breakpoint é como você garante que o mesmo dashboard fique bom no celular e no monitor grande — sem escrever uma linha de CSS de media query.
Painéis que se ajustam à altura: fillable e height
Dashboards modernos preenchem a tela sem barras de rolagem estranhas. bslib tem um sistema de fill: contêineres "preenchíveis" distribuem a altura disponível entre os cards.
page_fillable(
layout_columns(
fill = TRUE, # cards dividem a altura da tela
card(full_screen = TRUE, # botão de expandir para tela cheia
card_header("Mapa"), leafletOutput("mapa")),
card(card_header("Tabela"), DTOutput("tab"))
)
)
full_screen = TRUE adiciona um botão que expande o card para ocupar toda a tela — um recurso "de graça" que os usuários adoram para examinar um gráfico ou mapa em detalhe.
Recursos estáticos: a pasta www/
Imagens, CSS e JS próprios vão numa pasta chamada www/ ao lado do app.R. Tudo nela é servido na raiz da URL do app — um arquivo www/logo.png é acessível como logo.png.
meu_app/
├── app.R
└── www/
├── logo.png
├── custom.css
└── script.js
ui <- page_sidebar(
title = "App",
tags$head(
tags$link(rel = "stylesheet", href = "custom.css"), # www/custom.css
tags$script(src = "script.js") # www/script.js
),
tags$img(src = "logo.png", height = "40px"), # www/logo.png
# ...
)
Regra: qualquer arquivo que o navegador precisa baixar (CSS, JS, imagens, fontes) vai em www/. Não use caminhos absolutos do sistema de arquivos — o navegador não os alcança. Essa organização vira parte da estrutura de pacote em Golem (Golem move isso para inst/app/www).
HTML sob medida quando os componentes não bastam
Às vezes você precisa de uma estrutura que nenhum componente pronto oferece. Como a UI é só HTML, basta construí-la:
# Um "badge" customizado que não existe pronto no bslib
badge <- function(texto, cor = "primary") {
tags$span(class = paste0("badge bg-", cor), texto)
}
ui <- page_fillable(
card(
card_header("Status", badge("Ativo", "success")),
"Conteúdo do card com um badge no cabeçalho."
)
)
Combinar componentes bslib com tags sob medida é o que dá liberdade total de layout mantendo a responsividade do Bootstrap. Em identidade visual damos estilo a esse HTML; em programação em Shiny, comportamento com JS.
Toda UI de Shiny é HTML gerado por funções R — os tags$* e atalhos como h1, div e p —, o que abre a porta para gerar HTML programaticamente, como um card por linha de dados com lapply e tagList. O layout se apoia no grid de 12 colunas do Bootstrap, com fluidRow e column(width=) distribuindo espaço e empilhando em telas pequenas. A forma moderna é o bslib, com page_sidebar, page_navbar, page_fillable, card, value_box e layout_columns. A responsividade de verdade vem de col_widths = breakpoints(sm=, md=, lg=), que dá larguras diferentes por tamanho de tela sem nenhuma media query escrita à mão; e para distribuir altura, page_fillable com fill = TRUE, tendo full_screen = TRUE para expandir um card. Recursos estáticos — CSS, JS, imagens, fontes — vão em www/ e são servidos na raiz da URL; caminho absoluto do disco nunca funciona. E quando o componente pronto não basta, construir o HTML com tags dá liberdade total sem abrir mão da responsividade.
Exercícios
-
O que realmente é retornado quando você chama
fluidPage(h1("Oi"))no console R? Por que isso é importante?✓ Resposta:É retornado HTML — uma
<div class="container-fluid"><h1>Oi</h1></div>(um objetoshiny.tagque se imprime como HTML). Isso é importante porque revela que a UI do Shiny não é uma abstração mágica: são funções R que produzem HTML. Consequência prática: você pode gerar UI programaticamente (comlapply/tagList), inspecioná-la, e construir qualquer estrutura comtags$*quando os layouts prontos não bastarem. -
No grid do Bootstrap, você quer dois gráficos lado a lado ocupando metade da largura cada. Como escreve? E o que acontece no celular?
✓ Resposta:fluidRow(column(6, plotOutput("g1")), column(6, plotOutput("g2")))— cada coluna ocupa 6 das 12 unidades (50%). No celular (largura pequena), o Bootstrap empilha automaticamente as colunas: cada gráfico passa a ocupar a linha inteira, um sobre o outro. Esse empilhamento responsivo é o comportamento padrão do grid de 12 colunas. -
Qual a diferença entre
page_sidebar,page_navbarepage_fillablenobslib?✓ Resposta:page_sidebarcria uma página com barra lateral de filtros/controles (que colapsa em telas pequenas) e uma área principal.page_navbarorganiza o conteúdo em abas no topo (navbar), boa para apps com várias seções.page_fillablefaz o conteúdo preencher a altura da tela sem rolagem, ideal para dashboards de painéis que dividem o espaço. Escolhe-se pela estrutura de navegação desejada: filtros laterais, abas, ou painel único preenchível. -
Como você faz três
value_boxficarem lado a lado no desktop, dois-e-um no tablet e empilhados no celular?✓ Resposta:Com
layout_columnsecol_widths = breakpoints(...):layout_columns( col_widths = breakpoints(sm = c(12,12,12), md = c(6,6,12), lg = c(4,4,4)), value_box(...), value_box(...), value_box(...) )lg(desktop) dá 4+4+4 = três lado a lado;md(tablet) dá 6+6+12 = dois em cima e um embaixo;sm(celular) dá 12+12+12 = cada um na sua linha. Tudo isso sem escrever media queries de CSS. -
Onde deve ficar o arquivo
custom.cssdo seu app, e como você o inclui na UI? Por que não pode ser um caminho absoluto do disco?✓ Resposta:Deve ficar na pasta
www/ao lado doapp.R(www/custom.css). Inclui-se comtags$head(tags$link(rel = "stylesheet", href = "custom.css"))— note que ohrefé relativo à raiz da URL, sem owww/. Não pode ser caminho absoluto do disco porque quem baixa o CSS é o navegador do usuário, que só acessa URLs servidas pelo app, não o sistema de arquivos do servidor. Tudo que o browser precisa baixar (CSS, JS, imagens, fontes) vai emwww/.