Customizar um tema com bslib — variáveis de Sass, fontes, cores, bs_theme() — funciona bem, até você ter o segundo dashboard. E o terceiro. E aí descobre que a cor primária da empresa está escrita à mão em oito app.R diferentes, que dois deles usam tons ligeiramente distintos porque alguém digitou o hexadecimal de memória, e que uma troca de identidade visual significa caçar código em oito repositórios.
O brand.yml ataca exatamente esse problema. É um arquivo de configuração — um único YAML — que descreve a identidade visual de uma organização: cores, tipografia, logotipos. As ferramentas leem esse arquivo e se configuram sozinhas. Um dashboard Shiny, um relatório Quarto e um gráfico estático passam a sair com a mesma cara sem que ninguém repita uma cor.
Descrever a identidade visual de uma organização num _brand.yml, aplicá-la a aplicações Shiny via bslib, entender como a mesma descrição atende a outras ferramentas, e organizar o arquivo para que ele seja compartilhável entre projetos.
O problema que ele resolve
Sem um padrão compartilhado, a identidade visual vive espalhada:
# app-vendas/app.R
tema <- bs_theme(primary = "#1f6feb", base_font = font_google("Inter"))
# app-rh/app.R
tema <- bs_theme(primary = "#1F6FEB", base_font = font_google("Inter"), secondary = "#6b7280")
# relatorio.qmd
# (um CSS próprio, com #1f6feb escrito em três lugares)
Três problemas, todos previsíveis: a cor está duplicada, as duas grafias do mesmo hexadecimal já denunciam que ninguém copiou do mesmo lugar, e o relatório Quarto não tem como reaproveitar nada do Shiny.
Com brand.yml, a identidade é declarada uma vez:
# _brand.yml
meta:
name: Acme Analytics
color:
palette:
azul-marca: "#1f6feb"
azul-escuro: "#0b3d91"
cinza: "#6b7280"
vermelho: "#d64545"
verde: "#2f9e44"
primary: azul-marca
secondary: cinza
success: verde
danger: vermelho
foreground: "#1a202c"
background: "#ffffff"
typography:
fonts:
- family: Inter
source: google
- family: JetBrains Mono
source: google
base:
family: Inter
size: 16px
headings:
family: Inter
weight: 700
monospace:
family: JetBrains Mono
logo:
images:
completo: logos/acme-completo.svg
simbolo: logos/acme-simbolo.svg
small: simbolo
medium: completo
large: completo
E o app fica assim:
library(bslib)
ui <- page_navbar(
title = "Painel de Vendas",
theme = bs_theme(brand = TRUE), # lê o _brand.yml do projeto
nav_panel("Resumo", ...),
nav_panel("Detalhe", ...)
)
Uma linha. bs_theme(brand = TRUE) procura o _brand.yml a partir do diretório do projeto, lê cores, tipografia e logotipo, e traduz tudo para as variáveis do Bootstrap.
A estrutura do arquivo, seção por seção
color — paleta nomeada, papéis separados
A separação entre paleta e papéis é a ideia mais útil do formato:
color:
palette:
azul-marca: "#1f6feb"
vermelho: "#d64545"
primary: azul-marca
danger: vermelho
A palette dá nomes às cores da marca. Os papéis (primary, secondary, success, danger, warning, info, light, dark) apontam para esses nomes. O ganho aparece na manutenção: se a marca mudar o azul, você edita um hexadecimal e tudo que é primary acompanha — inclusive em projetos que nem foram abertos.
Também evita o erro clássico de usar a cor como se fosse o significado. Escrever primary: azul-marca diz por que aquela cor está ali; escrever #1f6feb espalhado pelo código não diz nada.
typography — fonte declarada, não baixada à mão
typography:
fonts:
- family: Inter
source: google
- family: Roboto Mono
source: bunny # alternativa ao Google Fonts
base:
family: Inter
size: 16px
line-height: 1.6
headings:
family: Inter
weight: 700
line-height: 1.2
monospace-inline:
color: azul-escuro
O source aceita google, bunny, file (fonte local) e system. A opção file importa num contexto corporativo: muitas empresas licenciam uma fonte própria que não está em CDN nenhum.
fonts:
- family: AcmeSans
source: file
files:
- path: fonts/AcmeSans-Regular.woff2
weight: 400
- path: fonts/AcmeSans-Bold.woff2
weight: 700
Note que monospace-inline pode ter cor própria — é o <code> no meio do texto, e distingui-lo do bloco de código é um detalhe de acabamento que quase ninguém lembra de fazer.
logo — um arquivo por tamanho
logo:
images:
completo: logos/acme-completo.svg
simbolo: logos/acme-simbolo.svg
small: simbolo
medium: completo
A distinção entre small, medium e large existe porque um logotipo horizontal com nome por extenso fica ilegível num favicon, e um símbolo isolado fica pobre num cabeçalho de relatório. Declarar os dois deixa cada ferramenta escolher o adequado.
Onde o arquivo mora
A busca é hierárquica, e isso permite o padrão que torna o formato útil de verdade:
- o diretório do app;
- os diretórios acima, até a raiz do projeto.
Num monorepo com vários dashboards, um _brand.yml na raiz serve a todos:
acme-analytics/
├── _brand.yml <- identidade compartilhada
├── logos/
├── app-vendas/
│ └── app.R
├── app-rh/
│ └── app.R
└── relatorios/
└── trimestral.qmd
Para times separados em repositórios diferentes, o caminho é empacotar a identidade — um pacote R interno que carrega o _brand.yml e o expõe:
# no pacote acmebrand
caminho_marca <- function() system.file("_brand.yml", package = "acmebrand")
# no app
theme = bs_theme(brand = acmebrand::caminho_marca())
Assim a identidade tem versão: atualizar a marca é subir a versão do pacote, e cada projeto adota quando puder.
Estender sem quebrar
O brand.yml cobre o essencial, não tudo. Ele convive bem com o ajuste específico:
tema <- bs_theme(brand = TRUE) |>
bs_add_rules(sass::sass_file("www/ajustes.scss"))
A ordem importa e a disciplina também: o _brand.yml é a fonte da verdade para cor e tipografia; o SCSS extra cuida de espaçamento, sombra, bordas e componentes próprios. No momento em que você escrever um hexadecimal no SCSS, terá reintroduzido o problema que o arquivo veio resolver. Se precisar da cor da marca no SCSS, use a variável Bootstrap que o brand.yml gerou:
.card-destaque {
border-left: 4px solid var(--bs-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, .06);
}
Alcançando os gráficos
Um dashboard com tema impecável e gráficos em cinza-padrão do ggplot2 fica visivelmente remendado. O pacote thematic resolve isso alinhando os gráficos ao tema ativo:
library(thematic)
thematic_shiny(font = "auto") # uma linha, no início do app
A partir daí, ggplot2, gráficos da base e lattice herdam fundo, cor de texto e fonte do tema — que, por sua vez, veio do _brand.yml.
Para as escalas de cor dos gráficos, vale definir explicitamente a partir da paleta, em vez de confiar no padrão:
cores_marca <- c("#1f6feb", "#2f9e44", "#d64545", "#6b7280")
ggplot(dados, aes(mes, valor, colour = categoria)) +
geom_line(linewidth = 1) +
scale_colour_manual(values = cores_marca)
Uma ressalva de acessibilidade que o brand.yml não faz por você: uma paleta de marca não é necessariamente distinguível por quem tem daltonismo, e as cores institucionais raramente foram escolhidas pensando nisso. Verifique contraste e, para séries categóricas, prefira paletas testadas — ou combine cor com forma e rótulo, de modo que a cor não seja o único canal de informação.
Além do Shiny
O mesmo _brand.yml é lido por outras ferramentas do ecossistema — documentos Quarto, por exemplo, aplicam a identidade sem configuração adicional além da referência ao arquivo. Esse é o argumento real do formato: a marca deixa de ser uma propriedade de cada aplicação e passa a ser um ativo do projeto, versionado junto do código, com histórico e revisão como qualquer outro arquivo.
O formato é novo e continua evoluindo. Vale acompanhar a especificação antes de apostar em campos menos comuns.
O brand.yml descreve a identidade visual num único YAML lido por várias ferramentas; no Shiny, basta bs_theme(brand = TRUE). O coração do formato é a separação entre paleta nomeada e papéis — primary, danger e companhia: trocar a cor da marca vira editar um hexadecimal, e o nome documenta a intenção de cada uso. A seção typography declara fontes com source: google, bunny, file para fonte licenciada local, ou system, e ainda permite estilo próprio para monospace-inline. O logo separa small, medium e large por uma razão prática: logotipo horizontal não serve de favicon. A busca do arquivo sobe do app até a raiz do projeto, então um _brand.yml na raiz de um monorepo serve a todos os apps; entre repositórios, empacotar a identidade num pacote R dá versionamento. Dá para estender com SCSS para espaçamento e componentes, mas sem escrever hexadecimal lá — use var(--bs-primary). O thematic_shiny() alinha ggplot2 e os gráficos da base ao tema. E uma ressalva que não se automatiza: acessibilidade continua sendo sua responsabilidade, porque paleta de marca não é automaticamente distinguível por daltônicos.
Exercícios
-
Qual a vantagem concreta de
primary: azul-marcasobreprimary: "#1f6feb"?✓ Resposta:Duas. Primeiro, manutenção: a cor fica definida num único ponto (a
palette), e todos os papéis que a usam — que podem ser vários, comoprimarye a cor de um link — apontam para o mesmo nome. Trocar o azul da marca vira uma edição de um hexadecimal, sem risco de esquecer uma ocorrência. Segundo, significado:azul-marcadiz de onde a cor vem e por que está ali, enquanto#1f6febé opaco. Isso importa quando outra pessoa abre o arquivo meses depois e precisa decidir se aquela cor pode mudar. -
Sua empresa licenciou a fonte AcmeSans, que não está em CDN nenhum. Como declará-la?
✓ Resposta:Com
source: file, listando os arquivos por peso dentro do próprio projeto:typography: fonts: - family: AcmeSans source: file files: - path: fonts/AcmeSans-Regular.woff2 weight: 400 - path: fonts/AcmeSans-Bold.woff2 weight: 700 base: family: AcmeSansDeclarar cada peso separadamente evita que o navegador sintetize o negrito esticando a fonte regular, o que produz um resultado visivelmente pior. Vale conferir também os termos da licença: muitas licenças de fonte comercial restringem o uso em aplicação web pública, e hospedar o arquivo
.woff2num site aberto é justamente esse caso. -
Por que escrever
#1f6febdentro do SCSS extra anula boa parte do benefício dobrand.yml?✓ Resposta:Porque reintroduz a duplicação que o arquivo existe para eliminar. A partir daí a cor da marca passa a morar em dois lugares — o
_brand.ymle o SCSS —, e a próxima troca de identidade visual vai atualizar um e esquecer o outro, produzindo o app com dois tons de azul ligeiramente diferentes. A forma correta é consumir a variável que obrand.ymljá gerou:var(--bs-primary)em CSS, ou a variável Sass correspondente. Assim o SCSS cuida do que é dele — espaçamento, sombra, borda, componentes próprios — sem opinar sobre cor. -
Como compartilhar uma identidade visual entre repositórios diferentes, e qual a vantagem sobre copiar o arquivo?
✓ Resposta:Empacotando a identidade num pacote R interno que carrega o
_brand.ymle os logotipos, e expõe o caminho — algo comobs_theme(brand = acmebrand::caminho_marca()). A vantagem sobre copiar é o versionamento: a marca passa a ter número de versão e histórico, cada projeto declara de qual versão depende, e a atualização é uma subida de versão controlada em vez de uma caçada por cópias desatualizadas. Copiar o arquivo garante que, em seis meses, existirão cinco variações levemente diferentes espalhadas, e ninguém saberá qual é a correta. -
O
brand.ymlgarante que o dashboard seja acessível? Justifique.✓ Resposta:Não. Ele garante consistência, que é outra coisa. As cores institucionais são escolhidas por critério de marca — reconhecimento, diferenciação, história — e raramente por contraste ou distinguibilidade para daltônicos. Aplicar fielmente uma paleta ruim nesse aspecto apenas propaga o problema para todos os projetos com mais eficiência. Permanecem responsabilidade de quem constrói: verificar contraste texto/fundo contra o critério WCAG, escolher escalas categóricas distinguíveis (ou paletas testadas para isso), e não usar cor como único canal de informação — combinando-a com forma, rótulo ou padrão, de modo que a informação sobreviva a uma impressão em preto e branco.