Na Wonderlabs, construí e mantive a biblioteca de componentes Angular do time sobre Angular Material, para um PWA de cartão de crédito que atendia mais de 50.000 usuários. O Storybook entrou só um pouco, perto do final. O objetivo era um sistema que se mantivesse alinhado com a biblioteca do Figma, que nós três do front-end pudéssemos consumir sem lutar contra a API, e que se mantivesse consistente conforme as features eram entregues. Aqui está o que me mordeu e o que eu faria diferente.
Arquitetura de Temas do Angular Material 17+
O Angular Material 17.2 introduziu suporte experimental ao Material Design 3 via um novo sistema de tokens baseado em CSS custom properties. O MD3 só se tornou estável no Angular Material 18. Na prática, se você está em 17.x, precisa optar explicitamente pela API experimental e aceitar que breaking changes podem acontecer. A partir da v18, o fluxo com define-theme produz CSS custom properties com escopo, os estilos dos componentes consomem essas propriedades, e chega de caçar variáveis Sass profundas quando uma cor precisa mudar. A biblioteca da Wonderlabs é anterior a isso: ela vivia no Angular 15 e na API Sass mostrada abaixo. A lição sobre tokens que vem a seguir se aplica às duas. Os trechos de componentes mais abaixo são ilustrativos e usam a sintaxe atual do Angular (blocos @if, inputs obrigatórios) e os nomes atuais de tokens do Material; no Angular 15 você escreveria *ngIf e @Input() puro.
// styles/_theme.scss
@use '@angular/material' as mat;
$primary-palette: mat.define-palette(mat.$indigo-palette, 700, 400, 900);
$accent-palette: mat.define-palette(mat.$amber-palette, A200);
$light-theme: mat.define-light-theme((
color: (
primary: $primary-palette,
accent: $accent-palette
),
typography: mat.define-typography-config(
$font-family: 'Inter, sans-serif',
$headline-4: mat.define-typography-level(2rem, 2.5rem, 700),
$body-1: mat.define-typography-level(1rem, 1.5rem, 400)
),
density: 0
));
@include mat.all-component-themes($light-theme);Copie esse bloco para um Material atual e ele não compila. O Angular Material 18 renomeou a API M2 com um prefixo m2-: mat.define-palette virou mat.m2-define-palette, mat.define-light-theme virou mat.m2-define-light-theme, mat.define-typography-config virou mat.m2-define-typography-config, e as paletas embutidas também ganharam o prefixo, então mat.$indigo-palette agora é mat.$m2-indigo-palette. Mesmos argumentos, nomes novos. Se você está começando um tema hoje, pule o M2 inteiro e use a API M3 define-theme.
Tokens Primeiro, Componentes Depois
O erro que cometi no início: comecei pelos componentes. Parecia produtivo. Meses depois tínhamos um botão, um form field, um modal e uma tabela de dados, todos construídos antes de a estrutura de tokens estar resolvida, e formalizar os tokens significou voltar em cada um deles para substituir valores hardcoded. Token-first não é conselho teórico, é cicatriz. A regra geral: sem tokens semânticos, uma mudança de cor da marca é uma caçada a hexadecimais hardcoded em cada componente; com eles, é uma edição só.
A camada de tokens fica em um único arquivo importado antes de qualquer estilo de componente, seja você escrevendo ele à mão ou gerando a partir das Figma Variables com o Style Dictionary. Valores brutos mapeiam para nomes semânticos. Nomes semânticos são o que os componentes consomem. Quando a cor da marca muda, você altera um valor bruto e a cascata semântica faz o resto.
/* styles/_tokens.scss */
:root {
/* Raw color scale */
--color-primary-500: #3949ab;
--color-primary-700: #283593;
--color-primary-900: #1a237e;
--color-error-600: #d32f2f;
--color-error-700: #c62828;
/* Semantic tokens, the only layer components reference */
--color-action-primary: var(--color-primary-700);
--color-action-primary-hover: var(--color-primary-900);
--color-text-primary: #1a1a2e;
--color-text-secondary: #4a4a6a;
--color-surface: #ffffff;
--color-surface-variant: #f4f4f8;
/* Spacing scale */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
/* Border radii */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 16px;
--radius-pill: 9999px;
}Estendendo o Material Sem Esconder Sua API
Um erro comum é envolver componentes do Material em wrappers gordos que expõem uma API completamente customizada. O desenvolvedor consumidor agora tem que aprender seu wrapper e perdeu acesso a todos os inputs do Material que realmente precisa. Eles começam a furar a abstração com ViewChild e ElementRef. Você piorou as coisas.
A abordagem melhor é usar wrappers finos que adicionam o comportamento específico necessário e deixam o Material fazer o resto. Para customização de variantes, CSS custom property overrides com escopo em um atributo host funcionam de forma limpa sem tocar na superfície TypeScript do componente.
// ds-button.component.ts: a thin wrapper, not a replacement
@Component({
selector: 'ds-button',
standalone: true,
imports: [MatButtonModule],
host: { '[attr.variant]': 'variant' },
template: `
<button mat-flat-button [disabled]="disabled">
<ng-content />
</button>
`
})
export class DsButtonComponent {
@Input() variant: 'primary' | 'secondary' | 'destructive' | 'ghost' = 'primary';
@Input() disabled = false;
}
// ds-button.component.scss
:host([variant='destructive']) {
--mat-button-filled-container-color: var(--color-error-600);
--mat-button-filled-label-text-color: white;
&:hover button {
--mat-button-filled-container-color: var(--color-error-700);
}
}Campos de Formulário com Tratamento Consistente de Erros
Em um produto de cartão de crédito, mensagens de erro de validação não são um detalhe secundário de UX. Depois que o texto foi revisado, ele não deveria variar de tela para tela. Um wrapper de form field no design system centraliza isso: um único lugar onde toda a lógica de mensagens de erro vive, com chaves de i18n, para que ninguém no time consiga acidentalmente entregar um texto fora do roteiro.
@Component({
selector: 'ds-form-field',
standalone: true,
imports: [MatFormFieldModule, MatInputModule, ReactiveFormsModule, TranslateModule],
template: `
<mat-form-field appearance="outline" [subscriptSizing]="'dynamic'">
<mat-label>{{ label | translate }}</mat-label>
<!-- The wrapper renders the input itself: mat-form-field cannot find
a matInput that is projected into it through ng-content -->
<input matInput [formControl]="control" />
@if (control.hasError('required') && control.touched) {
<mat-error>{{ 'validation.required' | translate }}</mat-error>
}
@if (control.hasError('minlength') && control.touched) {
<mat-error>
{{ 'validation.minLength' | translate:{ min: control.errors?.['minlength']?.requiredLength } }}
</mat-error>
}
</mat-form-field>
`
})
export class DsFormFieldComponent {
@Input({ required: true }) label!: string;
@Input({ required: true }) control!: FormControl;
}O Que Realmente Importa em Escala
- Versione a biblioteca como um pacote npm separado desde o primeiro dia. Mesmo que comece em um monorepo, o versionamento cria responsabilidade sobre breaking changes
- Documente cada variante de componente antes de implementar. Elas são documentação viva e critérios de aceite simultaneamente
- WCAG 2.1 AA é inegociável em aplicações financeiras. Atributos ARIA, navegação por teclado e taxas de contraste são incorporados nos componentes, não deixados para os times de produto
- Nomes de tokens semânticos sobrevivem a rebrands. --color-action-primary sobrevive a uma atualização de marca. --color-indigo-700 não
- Todo novo componente adicionado à biblioteca passava por uma revisão de design e pelo code review de um segundo desenvolvedor, mesmo em um time de três pessoas. Sem processo, o DS vira um depósito de lixo
- Quando você renomeia um componente ou muda uma API, forneça um schematic para que os times consumidores não fiquem presos em versões antigas indefinidamente
Um design system não é uma biblioteca de componentes. É uma linguagem compartilhada entre design e engenharia. Se designers não estão contribuindo para ele e desenvolvedores não estão consultando-o primeiro, ele já falhou.
Angular Material lida com acessibilidade, infraestrutura de temas e comportamento de componentes em um nível de qualidade que eu não vou replicar do zero. Construir em cima dele é a decisão certa. O investimento real está no sistema de tokens, no design de API dos componentes, na documentação e na governança que o mantém vivo e confiável. Acerte isso e a implementação técnica é a parte fácil.
Referências
- Angular Material theming. https://material.angular.io/guide/theming
- Material Design 3 tokens. https://m3.material.io/foundations/design-tokens/overview
- Storybook for Angular. https://storybook.js.org/docs/angular/get-started/introduction
- Angular CLI schematics. https://angular.dev/tools/cli/schematics