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:

  1. o diretório do app;
  2. 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

  1. Qual a vantagem concreta de primary: azul-marca sobre primary: "#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, como primary e 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-marca diz 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.

  2. 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: AcmeSans

    Declarar 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 .woff2 num site aberto é justamente esse caso.

  3. Por que escrever #1f6feb dentro do SCSS extra anula boa parte do benefício do brand.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.yml e 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 o brand.yml já 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.

  4. 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.yml e os logotipos, e expõe o caminho — algo como bs_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.

  5. O brand.yml garante 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.

Fontes e leituras recomendadas