This content originally appeared on DEV Community and was authored by Victor Assis
Um Design System é reconhecido por suas vantagens na construção de aplicações front-end. Entre elas, a padronização visual é, na prática, o benefício mais relevante; os ganhos de velocidade acabam surgindo como consequência. No entanto, para além da padronização, há um aspecto cada vez mais estratégico: a coleta e análise de dados sobre o uso do DS.
Neste artigo, discuto a importância de tornar um Design System orientado por dados, apresento quais métricas podem ser monitoradas e como essas informações apoiam decisões no ciclo de vida do software, além de como podemos coletar na prática essas informações.
Por que (e quais) dados coletar?
Assim como em qualquer produto digital, os dados são fundamentais para guiar decisões e avaliar impacto. No caso de um Design System, eles permitem entender como a solução é utilizada, mensurar seu valor nas aplicações e reduzir riscos em processos de atualização.
Embora aqui o foco esteja em um contexto web (HTML, CSS, JS/TS), os conceitos são aplicáveis a outros ambientes — de Android (Kotlin) a iOS (Swift), multiplataforma com Flutter ou até soluções de infraestrutura. Para este exercício, consideramos um cenário empresarial com um DS proprietário.
Os principais dados a serem analisados incluem:
Linguagem: tecnologias adotadas pelo app.
Framework/biblioteca e versões: por exemplo, Angular, React ou Vue; essas informações permitem prever impactos em atualizações.
Bibliotecas instaladas: identificação de outros DS ou soluções que convivem no mesmo projeto.
Componentes do DS: número de instâncias e propriedades utilizadas, principal métrica de adoção.
Componentes internos: elementos criados fora do DS, úteis para detectar gaps e necessidades de evolução.
Componentes desconhecidos: padrões emergentes que podem inspirar a inclusão de novas peças no DS.
Como coletar os dados
Tendo em mente quais informações queremos extrair, precisamos avaliar onde e como a coleta deve acontecer. Ferramentas de mercado como Google Analytics, FullStory ou Microsoft Clarity podem parecer opções viáveis, mas não atendem bem ao nosso objetivo. Isso porque elas são voltadas à análise de comportamento do usuário e, mesmo com customizações, não oferecem dados realmente confiáveis para medir o uso de um Design System. Os principais problemas são:
Execução após o build: em aplicações baseadas em JSX/TSX, referências a componentes do DS são removidas quando não se trata de tags customizadas. Após o build, perdemos o vínculo direto com o código-fonte.
Excesso de dados: a coleta em tempo de execução dispara registros a cada acesso de usuário, gerando redundância e necessidade de filtros.
Alto custo: muitas dessas ferramentas cobram por requisição ou volume de dados. Para nosso caso, em que basta coletar uma instância consolidada, esse custo é desnecessário.
Por esses motivos, a coleta deve ocorrer entre o desenvolvimento e a publicação, ou seja, durante o processo de CI/CD. Nesse estágio, conseguimos analisar o código-fonte de forma íntegra, sem ruído de execução em produção e sem custos adicionais. No nosso caso, a estratégia será implementar a coleta diretamente em pipelines de build — por exemplo, no GitHub Actions.
Implementação em Código
Existem diferentes estratégias para coletar os dados de um Design System no código-fonte. Uma abordagem simples seria converter tudo para string e usar expressões regulares para buscar prefixos e listar componentes, ou até manter uma allowlist com os nomes de cada componente a serem analisados.
Entretanto, essas soluções não escalam bem. Um caminho mais robusto é utilizar Abstract Syntax Tree (AST) ou mesmo ferramentas como o ANTLR (ANother Tool for Language Recognition). O AST é uma estrutura de dados hierárquica que representa o código em forma de árvore: cada nó corresponde a uma construção da linguagem (função, variável, chamada de componente etc.). Isso nos permite inspecionar o código de forma confiável e precisa.
Prefixos
Por convenção, muitos Design Systems usam prefixos para diferenciar seus componentes, como mat-
, ng-
, cbs-
. Para este artigo, adotaremos o prefixo fictício abs-.
Componentes internos
Antes de mapear os componentes do DS, precisamos identificar os componentes criados internamente na aplicação. Isso depende do framework:
Angular: podemos ler o angular.json e, nos arquivos, inspecionar o decorator
@Component
, extraindo o valor doselector
.React: cada componente é uma função ou classe exportada; logo, precisamos mapear todas as declarações de função que representam componentes.
Outros frameworks: cada biblioteca pode ter sua própria convenção, mas o objetivo é sempre identificar os elementos internos para diferenciá-los do DS.
Com esses dados, conseguimos traçar um paralelo: quais componentes vêm do DS, quais são internos e quais são externos/desconhecidos.
Parser HTML
Nos arquivos HTML, queremos identificar tags que representam componentes do DS. Para isso podemos usar a biblioteca htmlparser2
junto com css-select
.
Tags customizadas: normalmente seguem o padrão prefixo-nome, como
abs-input
.Diretivas e atributos: em Angular, por exemplo, precisamos capturar
absButton
,absMask
e outras diretivas.Lógica de contagem: percorremos todas as tags, filtramos as que começam com o prefixo definido (
abs-
) e incrementamos um contador em um objeto. Assim, cada chave representa o nome do componente e o valor, o número de instâncias detectadas.
Além disso, também podemos capturar classes CSS e atributos para enriquecer as métricas.
import { parseDocument } from "htmlparser2";
import { selectAll } from "css-select";
/**
* @param {string} html - conteúdo HTML
* @param {string[]} dsPrefixes - ex: ['abs']
* @param {Set<string>} discoveredAngularSelectorsSet - Set of discovered Angular selectors.
* @returns {{
* components: Record<string, number>,
* propValues: Record<string, Record<string, string[]>>,
* directives: Record<string, number>, // Corrected type from previous subtasks
* outsideComponents: Record<string, number>,
* internalComponents: Record<string, number>
* }}
*/
export function extractHtmlUsage(
html,
dsPrefixes = [],
discoveredAngularSelectorsSet = new Set()
) {
const tagPrefixes = dsPrefixes.map((p) => p + "-");
const htmlDirectivePrefixes = dsPrefixes.map((p) => p.toLowerCase());
const result = {
components: {},
propValues: {},
directives: {},
outsideComponents: {},
internalComponents: {},
classes: {},
};
const doc = parseDocument(html);
const elements = selectAll("*", doc);
for (const el of elements) {
if (!el.name || !el.attribs) continue;
const tag = el.name;
const isCustomElement = tag.includes("-");
const isDSComponent = tagPrefixes.some((prefix) => tag.startsWith(prefix));
if (isDSComponent) {
result.components[tag] = (result.components[tag] || 0) + 1;
if (!result.propValues[tag]) result.propValues[tag] = {};
for (const [attr, val] of Object.entries(el.attribs)) {
const cleanAttr = attr.replace(/[\[\]\(\)\*]/g, "");
if (typeof val === "string") {
if (!result.propValues[tag][cleanAttr])
result.propValues[tag][cleanAttr] = [];
if (!result.propValues[tag][cleanAttr].includes(val)) {
result.propValues[tag][cleanAttr].push(val);
}
}
}
} else if (discoveredAngularSelectorsSet.has(tag)) {
result.internalComponents[tag] =
(result.internalComponents[tag] || 0) + 1;
} else if (isCustomElement) {
result.outsideComponents[tag] = (result.outsideComponents[tag] || 0) + 1;
}
const classAttr = el.attribs.class;
if (classAttr) {
const classes = classAttr.split(/\s+/);
for (const cls of classes) {
if (dsPrefixes.some((prefix) => cls.startsWith(prefix + "-"))) {
result.classes[cls] = (result.classes[cls] || 0) + 1;
}
}
}
for (const attr of Object.keys(el.attribs)) {
const cleanAttr = attr.replace(/[\[\]\(\)\*]/g, "");
if (
htmlDirectivePrefixes.some(
(p) => cleanAttr.startsWith(p) && cleanAttr.length > p.length
)
) {
result.directives[cleanAttr] = (result.directives[cleanAttr] || 0) + 1;
}
}
}
return result;
}
O parser varre o DOM, incrementa contadores por componente DS, coleta props/atributos, contabiliza diretivas com prefixo, registra classes do DS e separa componentes internos (já descobertos) de externos.
Parser JS/TS
Nos arquivos JavaScript e TypeScript, a ideia é percorrer a árvore AST (com ferramentas como @babel) e identificar:
Instâncias de componentes do DS.
Diretivas ou props aplicadas.
Componentes internos.
Qualquer outro elemento não mapeado, tratado como componente externo.
import fs from "fs";
import * as babelParser from "@babel/parser";
import traverseModule from "@babel/traverse";
const traverse = traverseModule.default;
/**
* Extrai componentes, props, diretivas e componentes internos de um arquivo JSX/TSX.
* @param {string} filePath - Caminho do arquivo.
* @param {string[]} dsPrefixes - Prefixos dos componentes e diretivas do DS, ex: ['abs']. (Note: para diretivas, o prefixo pode ser minúsculo ex: 'abs')
* @param {Set<string>} discoveredJsxInternalNamesSet - Set of discovered JSX internal component names.
* @returns {{
* components: Record<string, number>,
* propValues: Record<string, Record<string, (string | number | boolean)[]>>,
* directives: Record<string, number>,
* internalComponents: Record<string, number>,
* outsideComponents: Record<string, number>,
* classes: Record<string, number>
* }}
*/
export function extractJsxUsage(
filePath,
dsPrefixes = [],
discoveredJsxInternalNamesSet = new Set()
) {
const code = fs.readFileSync(filePath, "utf8");
const ast = babelParser.parse(code, {
sourceType: "module",
plugins: [
"jsx",
"typescript",
"decorators-legacy",
"deprecatedImportAssert",
],
});
const result = {
components: {},
propValues: {},
directives: {},
internalComponents: {},
outsideComponents: {},
classes: {},
};
const dsClassPrefixes = dsPrefixes.map((p) => `${p.toLowerCase()}-`);
const directiveAttributePrefixes = dsPrefixes.map((p) => p.toLowerCase());
traverse(ast, {
JSXOpeningElement(path) {
const nameNode = path.node.name;
if (!nameNode || !nameNode.name) return;
const tagName = nameNode.name;
const isDSComponent = dsPrefixes.some((p) => tagName.startsWith(p));
if (isDSComponent) {
result.components[tagName] = (result.components[tagName] || 0) + 1;
if (!result.propValues[tagName]) result.propValues[tagName] = {};
for (const attr of path.node.attributes) {
if (attr.type !== "JSXAttribute" || !attr.name) continue;
const propName = attr.name.name;
if (!propName) continue;
let value;
if (attr.value === null) {
value = true;
} else if (attr.value.type === "StringLiteral") {
value = attr.value.value;
} else if (attr.value.type === "JSXExpressionContainer") {
const expression = attr.value.expression;
if (
expression.type === "StringLiteral" ||
expression.type === "NumericLiteral" ||
expression.type === "BooleanLiteral"
) {
value = expression.value;
}
}
if (value !== undefined) {
if (!result.propValues[tagName][propName]) {
result.propValues[tagName][propName] = [];
}
if (!result.propValues[tagName][propName].includes(value)) {
result.propValues[tagName][propName].push(value);
}
}
}
} else if (discoveredJsxInternalNamesSet.has(tagName)) {
result.internalComponents[tagName] =
(result.internalComponents[tagName] || 0) + 1;
} else {
if (/^[A-Z]/.test(tagName)) {
result.outsideComponents[tagName] =
(result.outsideComponents[tagName] || 0) + 1;
}
}
for (const attr of path.node.attributes) {
if (attr.type === "JSXAttribute" && attr.name && attr.name.name) {
const propName = attr.name.name;
if (
directiveAttributePrefixes.some(
(prefix) =>
propName.toLowerCase().startsWith(prefix) &&
propName.length > prefix.length
)
) {
result.directives[propName] =
(result.directives[propName] || 0) + 1;
}
}
}
for (const attr of path.node.attributes) {
if (
attr.type === "JSXAttribute" &&
attr.name &&
(attr.name.name === "className" || attr.name.name === "class")
) {
const processClasses = (classString) => {
if (typeof classString !== "string") return;
const classes = classString.split(/\s+/).filter(Boolean);
for (const cls of classes) {
if (dsClassPrefixes.some((p) => cls.startsWith(p))) {
result.classes[cls] = (result.classes[cls] || 0) + 1;
}
}
};
if (attr.value) {
if (attr.value.type === "StringLiteral") {
processClasses(attr.value.value);
} else if (attr.value.type === "JSXExpressionContainer") {
const expression = attr.value.expression;
if (expression.type === "StringLiteral") {
processClasses(expression.value);
} else if (expression.type === "TemplateLiteral") {
expression.quasis.forEach((quasi) => {
processClasses(quasi.value.cooked);
});
}
}
}
break;
}
}
},
});
return result;
}
Extra – Parser CSS/SCSS
Nos Design Systems modernos, tokens também são elementos importantes a monitorar. Podemos buscar ocorrências de:
Custom Properties:
var(--prefix-*)
Variáveis SCSS:
$prefix-*
Essas métricas ajudam a entender a adoção dos tokens de estilo.
/**
* Analisa conteúdo CSS/SCSS para detectar tokens do DS por prefixo
* @param {string} content - Conteúdo do arquivo
* @param {string} prefix - Prefixo do DS (ex: 'abs')
* @returns {{
* customProperties: string[],
* scssVariables: string[]
* }}
*/
export function extractCssTokens(content, prefix) {
const customProperties = new Set();
const scssVariables = new Set();
const classes = new Set();
const varRegex = new RegExp(
`var\\(\\s*(--${prefix}-[a-z0-9-_]+)\\s*\\)`,
"gi"
);
for (const match of content.matchAll(varRegex)) {
customProperties.add(match[1]);
}
const scssRegex = new RegExp(`\\$${prefix}-[a-z0-9-_]+`, "gi");
for (const match of content.matchAll(scssRegex)) {
scssVariables.add(match[0]);
}
const classSelectorRegex = new RegExp(`\\.(${prefix}-[a-zA-Z0-9-_]+)`, "g");
for (const match of content.matchAll(classSelectorRegex)) {
if (match[1]) {
classes.add(match[1]);
}
}
return {
customProperties: [...customProperties],
scssVariables: [...scssVariables],
classes: [...classes],
};
}
O parser lista custom properties, variáveis SCSS e classes utilitárias usadas, revelando a maturidade dos tokens (cores, spacing, tipografia) e pontos de inconsistência.
GitHub Action
A execução desse código na pipeline é bem simples, depois de centralizar o projeto, podemos subir um ambiente nodejs, e executar quando tivermos uma pr para a branch principal:
name: Reusable DS Usage Analyzer
on:
workflow_call:
inputs:
node-version:
required: false
type: string
default: '18'
jobs:
analyze-ds-usage:
runs-on: ubuntu-latest
steps:
- name: Checkout target repository
uses: actions/checkout@v4
- name: Checkout ds-usage-analyzer logic
uses: actions/checkout@v4
with:
repository: victor-assis/Design-System-Metrics
path: analyzer
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Install analyzer dependencies
run: cd analyzer && npm i
- name: Run Web Scanner
run: node analyzer/scripts/scan-web.js
- name: Generate Final Report and Commit
run: node analyzer/scripts/generate-report.js
- name: Upload JSON Report
uses: actions/upload-artifact@v4
with:
name: ds-usage-report-json
path: analyzer/reports/final-report.json
- name: Upload Markdown Report
uses: actions/upload-artifact@v4
with:
name: ds-usage-report-md
path: analyzer/reports/final-report.md
- name: Comment on PR with report
if: github.event_name == 'pull_request'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: node analyzer/scripts/comment-pr.js
Abaixo, o passo a passo dos steps do seu workflow:
Checkout do repositório alvo –
Obtém o código da aplicação que será analisada (a “fonte” onde mediremos o uso do DS).Checkout da lógica do analisador –
Baixa o projeto do analisador (scripts scan-web.js, generate-report.js, etc.) em analyzer/, separando app e ferramenta.Setup Node –
Define a versão do Node para rodar os scripts de análise com previsibilidade.Instalação de dependências do analisador –
Executa npm i dentro de analyzer/ para preparar parsers (Babel, htmlparser2, etc.).Execução do Web Scanner –
Roda scan-web.js para vasculhar HTML/JS/TS/CSS/SCSS, identificar componentes DS, internos, externos e tokens, e gerar resultados intermediários (ex.: JSON em analyzer/reports/).Geração do relatório final –
generate-report.js consolida tudo em JSON e Markdown (ex.: final-report.json e final-report.md), prontos para stakeholders.Upload do relatório JSON (artifact) –
Publica o JSON como artefato do workflow (útil para integrações ou ingestão posterior em dashboards).Upload do relatório Markdown (artifact) –
Publica a versão legível (MD) para leitura rápida.Comentário no PR com o relatório (condicional a pull_request) –
Usa GITHUB_TOKEN para postar um resumo do relatório diretamente na discussão do PR — visibilidade imediata para devs, design e produto.
Resumo da integração: a análise roda antes do merge, dando uma “fotografia” confiável da adoção do DS no código-fonte (sem ruído de runtime), com custo zero adicional e alto sinal para tomada de decisão.
Exemplo do relatório legível como comentário na PR:
Análise dos dados
Com as informações coletadas, conseguimos extrair insights relevantes para a evolução do Design System. Por exemplo, se semanticamente uma das propriedades de um componente precisar mudar, conseguimos mensurar o impacto dessa atualização a partir dos números. Outro caso é o do nosso componente de ícones: com uma folha próxima de 300 ícones, podemos identificar quais não estão em uso e, assim, reduzir o tamanho do sprite e otimizar o bundle dos aplicativos.
Além disso, as métricas permitem calcular a economia aproximada de tempo proporcionada pelo uso de um Design System. Nesse caso, é importante refletir que cada componente não é recriado do zero a cada instância — ele é construído uma vez e depois reaproveitado. Assim, podemos modelar o cálculo em duas partes: o custo do primeiro uso e o custo marginal de reuso.
Uma fórmula possível para estimar esse ganho é:
Onde:
- PiP_iPi = número total de instâncias do componente no projeto.
- SiS_iSi = tempo para construir do zero um componente de complexidade CiC_iCi .
- bib_ibi = tempo do primeiro uso do componente no app (integração inicial).
- rir_iri = tempo de reuso marginal por instância adicional.
Para parametrizar:
- Si=Ci⋅VbuildS_i = C_i \cdot V_{\text{build}}Si=Ci⋅Vbuild
- bi=k1⋅Sib_i = k_1 \cdot S_ibi=k1⋅Si (ex.: 30% do esforço de build)
- ri=k2⋅Sir_i = k_2 \cdot S_iri=k2⋅Si (ex.: 5% do esforço de build)
Tabela-guia de complexidade:
Complexidade | Exemplos | CiC_iCi | SiS_iSi (semana base) |
---|---|---|---|
Baixa | Button, Tag, Badge | 1 | ~0.5 |
Média | Modal, Tabs, FormField | 2 | ~1.0 |
Alta | DataTable, DatePicker, RichEditor | 3 | ~1.5 |
Exemplo prático (com k₁=0,3 e k₂=0,05):
Botão (P=20, C=1) → Sem DS = 20 × 0,5 = 10 semanas
Com DS = 0,15 + 19 × 0,025 = 0,625 semanas
Economia ≈ 9,4 semanasTabela (P=5, C=3) → Sem DS = 5 × 1,5 = 7,5 semanas
Com DS = 0,45 + 4 × 0,075 = 0,75 semanas
Economia ≈ 6,75 semanas
Total estimado = ~16,1 semanas economizadas.
Conclusão
Um Design System vai além da padronização visual e da velocidade de entrega. Quando orientado por dados, ele se torna uma ferramenta estratégica, capaz de revelar como é realmente utilizado nas aplicações e de apoiar decisões fundamentadas para sua evolução.
Ao longo deste artigo, vimos que a coleta de métricas não precisa depender de ferramentas de mercado voltadas ao comportamento do usuário. Pelo contrário, a análise pode (e deve) acontecer no ciclo de desenvolvimento, aproveitando o código-fonte cru em pipelines de CI/CD. Essa abordagem garante dados consistentes, evita custos desnecessários e oferece uma visão precisa da adoção do DS.
A implementação prática mostrou como usar técnicas como AST, parsers de HTML/JS/SCSS e integração com GitHub Actions para extrair informações relevantes: quais componentes estão em uso, quais foram criados internamente, quais tokens de estilo foram aplicados e até mesmo quais padrões emergentes podem inspirar novos elementos no DS.
Mais do que medir, a chave está em transformar os dados em decisões: identificar gaps, priorizar melhorias, avaliar impacto de mudanças e, principalmente, alinhar times de design, produto e tecnologia em torno de um mesmo objetivo.
Um Design System data-driven não é apenas um repositório de componentes — é um mecanismo de feedback contínuo que fortalece a consistência, reduz riscos e potencializa a colaboração. Em outras palavras, é quando o Design System deixa de ser apenas uma “caixa de ferramentas” e se consolida como um ativo estratégico da organização.
Referências
Este projeto foi fruto de um estudo sobre como coletar métricas de um Design System, o código não está perfeito, mas pode ajudar times a evoluir o assunto.
GitHub do projeto: https://github.com/victor-assis/Design-System-Metrics
Exemplo de report em comentario: https://github.com/victor-assis/Design-System-Metrics/wiki/Exemplo-de-coment%C3%A1rio-da-analise
This content originally appeared on DEV Community and was authored by Victor Assis