Angular Material to świetne narzędzie do tworzenia intuicyjnych, responsywnych i atrakcyjnych interfejsów użytkownika. Oferuje gotowe, reużywalne komponenty UI, które są zgodne z wytycznymi Google Material Design.
Od wersji 18 Angular w pełni wspiera specyfikację Material Design 3, który opiera się na tokenach zaimplementowanych jako zmiennych CSS. Takie podejście pozwala definiować właściwości na najwyższym poziomie, które następnie propagują się do komponentów, które z nich korzystają. Dzięki tej aktualizacji możesz szczegółowo nadpisywać właściwości stylów bez zwiększania specyficzności selektorów CSS ani ingerowania w wewnętrzne selektory komponentów Angular Material.
Wersja 19 wprowadziła nowe API do swojego zaawansowanego systemu komponowania motywu, które umożliwia deweloperom dostosowanie wyglądu i stylu aplikacji. Oferuje on uporządkowane podejście do definiowania kolorów, typografii i stylów komponentów, zapewniając spójność w całej aplikacji przy jednoczesnym zachowaniu elastyczności w dostosowywaniu do preferencji marki i projektu.
Na pierwszy rzut oka może to wydawać się skomplikowane, więc podzielmy cały proces na części. Ten artykuł przeprowadzi Cię przez podstawy tworzenia motywów w Angular Material, pokazując, jak tworzyć, konfigurować i stosować własne motywy.
Wymaganie wstępne: Podstawy Sass
Sass to potężny preprocesor CSS, który rozszerza możliwości standardowego CSS, umożliwiając programistom pisanie czystszych, bardziej modularnych i łatwiejszych w utrzymaniu stylów. Angular Material wykorzystuje Sass w swojej strukturze, dlatego ważne jest, aby zdefiniować podstawowe pojęcia.
Zagnieżdżanie
System zagnieżdżania w Sass pozwala pisać style CSS w hierarchiczny i intuicyjny sposób, odzwierciedlający strukturę HTML. Zamiast wielokrotnego powtarzania selektorów, możesz zagnieżdżać style dla elementów podrzędnych bezpośrednio wewnątrz bloku ich elementu nadrzędnego.
Dokumentacja: https://sass-lang.com/documentation/style-rules/declarations/#nesting
.header {
width: 100vw;
&__logo {
margin-left: auto;
}
&--dark {
background-color: #3a2125;
color: white;
}
h1 {
font-size: 2rem;
line-height: 2.8rem;
&:hover {
text-decoration: underline;
}
}
}
Zmienne
Uprość zarządzanie arkuszami stylów, przechowując wartości takie jak kolory, odstępy, rozmiary i inne elementy projektu w wielokrotnie używanych, nazwanych zmiennych.
Dokumentacja: https://sass-lang.com/documentation/variables/
$my-color: #12ea8c;
$selector: primary;
.card {
$main-color: $my-color;
background-color: $main-color;
padding: 1rem;
}
// #{$variable} - string interpolation
.link-#{selector} {
color: $my-color;
}
Mapy
Mapa pozwala przechowywać i zarządzać danymi w postaci par klucz-wartość. Podobnie jak obiekty w JavaScript, mapa umożliwia organizowanie wartości w sposób strukturalny i łatwy do utrzymania
Dokumentacja: .https://sass-lang.com/documentation/values/maps/
$sizes: (
sm: 10rem;
md: 15rem;
lg: 20rem;
xl: 25rem;
);
@each $key, $value in $sizes {
.card--#{$key} {
width: $value;
}
}
.user-card {
width: map-get($sizes, md);
}
Listy
Listy służą do grupowania wielu wartości w proste struktury danych. Listy SCSS są elastyczne i mogą być oddzielone przecinkami (1px, 2px, 3px) lub spacjami (1px 2px 3px).
Dokumentacja: https://sass-lang.com/documentation/values/lists/
$size-prefixes: sm, md, lg;
.panel-#{nth($size-prefixes,2)} {
color: $primary-color;
width: 15rem;
}
Funkcje
Dzięki funkcjom możesz wykonywać operacje i zwracać wartości bezpośrednio w arkuszu stylów. Umożliwiają one dynamiczne stylowanie poprzez przetwarzanie danych, takich jak kolory, liczby, ciągi znaków, jednostki oraz struktury danych, jak mapy i listy. Obsługują również podstawowe koncepcje, takie jak logika boole’owska i pętle.
Dokumentacja: https://sass-lang.com/documentation/values/functions/
@function fade-out($color, $alpha: 0.5) {
@if $alpha < 0 or $alpha > 1 {
@error “Provide value between 0 and 1”;
}
@return rgba($color, $alpha);
}
@function sum($numbers...) {
$sum: 0;
@each $number in $numbers {
$sum: $sum + $number;
}
@return $sum;
}
.card {
background-color: fade-out($primary-color);
border: solid 1px fade-out($accent-color, 0.3);
width: sum(50px, $base-width, 10vw);
}
Mixins
Mixin to reużywalny blok CSS, który możesz zdefiniować raz i stosować tam, gdzie jest to potrzebne, redukując redundancję i sprawiając, że kod jest łatwiejszy do utrzymania. Mixiny są szczególnie przydatne do obsługi powtarzalnych wzorców, skomplikowanych kombinacji właściwości lub kompatybilności między przeglądarkami, ponieważ mogą przyjmować parametry do dostosowywania swojego zachowania.
Dokumentacja: https://sass-lang.com/documentation/values/mixins/
@mixin reset-list {
margin: 0;
padding: 0;
list-style: none;
}
@mixin horizontal-list($primary-element-color: currentColor) {
@include reset-list;
display: flex;
gap: 1rem;
li.primary {
color: $primary-element-color;
}
}
Moduły
Moduły pomagają skuteczniej organizować i zarządzać stylami, szczególnie w większych projektach. Dwie główne dyrektywy, @import i @use, pozwalają na dołączanie stylów z innych plików, ale różnią się funkcjonalnością i zastosowaniem.
Starsza dyrektywa, @import, pozwala na dołączanie stylów z jednego pliku do drugiego. Jednak ma pewne ograniczenia, takie jak wolniejsza kompilacja i potencjalne problemy z duplikowaniem stylów (kod Sass jest wykonywany przy każdym wywołaniu @import).
Dokumentacja: https://sass-lang.com/documentation/at-rules/import/
Wprowadzona jako nowoczesna alternatywa, dyrektywa @use oferuje lepszy sposób importowania stylów z przestrzenią nazw. Unika konfliktów, wymagając jawnych odniesień do importowanych zmiennych lub mixinów.
Dokumentacja: https://sass-lang.com/documentation/at-rules/use/
@import “./variables.scss”;
@use “./functions.scss”;
@use “./my-awesome-mixins.scss” as mixins;
.options-list {
@include mixins.horizontal-list(functions.fade-out($primary-color));
}
Definiowanie własnego motywu
Nowe API Angular Material znacznie upraszcza proces tworzenia motywów. Aby skorzystać z podstawowego motywu, wystarczy użyć mixinu theme w stylach z zakresem HTML, aby zapewnić jego zastosowanie w całej aplikacji. Mixin akceptuje mapę definiującą kolory, typografię i gęstość, a następnie generuje zestaw zmiennych CSS, które kontrolują wygląd i układ komponentów.
@use “@angular/material” as mat;
html {
@include mat.theme(
(
color: mat.$violet-palette,
typography: Roboto,
density: 0,
)
);
}
Oczywiście, możliwości dostosowania są o wiele większe. Najpierw przyjrzyjmy się dodatkowym funkcjom, jakie oferuje mixin theme.
Kolory
Kolory w motywie określają style kolorystyczne komponentów, takie jak kolor wypełnienia przycisków lub kolor obramowania pól tekstowych. Angular Material rozpoznaje następujące role kolorów:
- Primary (główny) – Jest to główny kolor marki, używany w najbardziej widocznych komponentach interfejsu użytkownika. Angular Material korzysta z „bazowego” koloru głównego oraz kilku jaśniejszych i ciemniejszych odcieni. Przykłady: przyciski, aktywne elemety
- Secondary (akcentujący) – Kolor służący do wyróżniania lub podkreślania elementów. Zwykle jest żywy i kontrastuje z kolorem głównym. Używany oszczędnie, aby nie przytłaczać użytkownika. Przykłady: przycisk FAB, aktywna zakładka
- Tertiary (trzeciorzędny) – Używany do kontrastowych akcentów, które równoważą kolory główny i akcentujący lub zwracają większą uwagę na określony element, taki jak pole tekstowe.
- Warn (ostrzegawczy) – Służy do komunikowania stanów błędów, np. w przypadku wprowadzenia nieprawidłowego hasła w polu tekstowym.
Każda rola opisana jest przez paletę kolorów – zestaw podobnych kolorów o różnych odcieniach, od ciemnych (najniższy indeks) po jasne (najwyższy indeks). Angular Material wykorzystuje te palety do tworzenia schematów kolorów, które komunikują hierarchię, stany oraz tożsamość marki w aplikacji.
Aby skomponować swój motyw, możesz skorzystać z jednej z wbudowanych palet kolorów: red (czerwony), green (zielony), blue (niebieski), yellow (żółty), cyan (błekitny), magenta (purpurowy), orange (pomarańczowy), chartreuse (jasnozielony), spring-green (wiosenna zieleń), azure (lazurowy), violet (fioletowy), rose (różany).
Aby uzyskać dostęp do palety, użyj zmiennej o nazwie “${{nazwaPalety}}-palette” z biblioteki @angular/material.
Jeśli potrzebujesz kolorów idealnie dopasowanych do Twojej marki, możesz stworzyć własną paletę, korzystając z tego generatora:
ng generate @angular/material:theme-color
Teraz możesz skonfigurować kolory, używając wygenerowanej palety głównej:
@use “@angular/material” as mat;
@use “./theme-colors” as theme;
html {
@include mat.theme(
(
color: theme.$primary-palette,
typography: Roboto,
density: 0,
)
);
}
Lub skonfiguruj paletę trzeciorzędną osobno, aby dodać wyraźny kolor akcentu do niektórych komponentów:
@use “@angular/material” as mat;
@use “./theme-colors” as theme;
html {
@include mat.theme(
(
color: (
primary: theme.$primary-palette,
tertiary: theme.$tertiary-palette
),
typography: Roboto,
density: 0,
)
);
}
Typografia
Typografia to system, który definiuje zestaw stylów fontów, aby zapewnić spójny i atrakcyjny wizualnie tekst w całej aplikacji. Tutaj możesz wybrać prosty sposób, definiując tylko rodzaj fontu:
@use “@angular/material” as mat;
@use “./theme-colors” as theme;
html {
@include mat.theme(
(
color: theme.$primary-palette,
typography: Poppins,
density: 0,
)
);
}
lub rozróżnić fonty dla tekstu podstawowego (używanego w większości tekstów aplikacji) i tekstu brandingowego (używanego w nagłówkach i tytułach):
@use “@angular/material” as mat;
@use “./theme-colors” as theme;
html {
@include mat.theme(
(
color: theme.$primary-palette,
typography: (
plain-family: Poppins,
brand-family: Montserrat,
),
density: 0,
)
);
}
Jeśli szukasz wysokiej jakości fontów, odwiedź Google Fonts, gdzie możesz zapoznać się z szerokim wyborem czcionek, korzystając z pomocnych narzędzi do podglądu. Wybierz potrzebne fonty i osadź wygenerowany kod do pobrania ich w sekcji <head> pliku index.html.
Kolejnym konfigurowalnym aspektem typografii jest grubość fontu (font weight), która pozwala na zdefiniowanie konkretnych grubości dla tekstu regularnego, średniego i pogrubionego:
@use “@angular/material” as mat;
@use “./theme-colors” as theme;
html {
@include mat.theme(
(
color: theme.$primary-palette,
typography: (
plain-family: Poppins,
brand-family: Montserrat,
bold-weight: 800,
medium-weight: 500,
regular-weight: 300,
),
density: 0,
)
);
}
Gęstość
Wartość gęstości (density) określa odstępy wewnątrz komponentów, takie jak odstępy wokół tekstu przycisku lub wysokość pól formularzy.
Przyjmuje wartości całkowite od 0 do -5, gdzie 0 oznacza domyślne odstępy, a -5 zapewnia najbardziej zwarty i kompaktowy układ. Każdy kolejny krok w dół (-1, -2 itd.) zmniejsza odpowiednie rozmiary o 4px, aż do minimalnego rozmiaru wymaganego do spójnego renderowania komponentów, co powoduje, że większość z nich zawiera mniej pustej przestrzeni w swoim układzie.
Motyw dostosowany do kontekstu
Nie musisz ograniczać swojej aplikacji do jednego motywu. Jeśli chcesz, aby konkretna sekcja wyróżniała się i przyciągała uwagę, możesz zastosować do niej inny motyw.
@use “@angular/material” as mat;
@use “./theme-colors” as theme;
html {
@include mat.theme(
(
color: theme.$primary-palette,
typography: Poppins,
density: 0,
)
);
}
.azure-section {
@include mat.theme(
(
color: mat.$azure-palette,
typography: Poppins,
density: 0,
)
);
}
Tryb ciemny
Tryb ciemny (Dark mode) to powszechnie używana funkcja, a Angular Material ułatwia jego implementację. Domyślnie wykorzystuje funkcję light-dark, która pozwala na określenie dwóch kolorów dla danej właściwości. Ta funkcja zwraca jeden z dwóch kolorów w zależności od tego, czy aktywny jest jasny czy ciemny schemat kolorów. Schemat jest określany na podstawie konfiguracji zdefiniowanej przez programistę lub preferencji użytkownika ustawionych w systemie operacyjnym lub przeglądarce.
Dodatkowo właściwość CSS color-scheme umożliwia zastąpienie schematu kolorów użytkownika na jasny lub ciemny. Ta elastyczność ułatwia tworzenie spójnych wizualnie i przyjaznych dla użytkownika interfejsów.
@use “@angular/material” as mat;
@use “./theme-colors” as theme;
@mixin apply-dark-mode {
color-scheme: dark;
}
html {
@include mat.theme(
(
color: theme.$primary-palette,
typography: Poppins,
density: -2,
)
);
}
body {
margin: 0;
&.dark-mode {
@include apply-dark-mode;
}
}
Dzięki tak zdefiniowanym stylom możesz zaimplementować przełącznik do zmiany między trybem jasnym i ciemnym, dynamicznie dodając lub usuwając klasę dark-mode z elementu <body>. Aby zapewnić jak najlepsze UX, początkowy tryb powinien być zgodny z ustawieniami urządzenia użytkownika.
export type ColorMode = 'light' | 'dark';
export const PREFERRED_COLOR_MODE = new InjectionToken<Signal<ColorMode>>(
'PREFERRED_COLOR_MODE',
{
providedIn: 'root',
factory: () => {
const destroyRef = inject(DestroyRef);
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const colorMode = signal<ColorMode>(
mediaQuery.matches ? 'dark' : 'light',
);
const preferredColorModeChangeListener = (event: MediaQueryListEvent): void => {
event.matches ? colorMode.set('dark') : colorMode.set('light');
};
mediaQuery.addEventListener('change', preferredColorModeChangeListener);
destroyRef.onDestroy(() =>
mediaQuery.removeEventListener('change', colorSchemeChangeListener),
);
return colorMode;
},
},
);
// Renderer2 cannot be directly injected into singleton service
export const injectRenderer2 = (): Renderer2 =>
inject(RendererFactory2).createRenderer(null, null);
@Injectable({ providedIn: 'root' })
export class DarkModeService {
private readonly DARK_MODE_CLASS = 'dark-mode';
private readonly _renderer = injectRenderer2();
private readonly _document = inject(DOCUMENT);
private readonly _preferredColorMode = inject(PREFERRED_COLOR_MODE);
private readonly _mode = linkedSignal(() => this._preferredColorMode());
readonly mode = this._mode.asReadonly();
readonly isDarkMode = computed(() => this.mode() === 'dark');
constructor() {
effect(() => {
this._applyDarkModeClass(this.isDarkMode());
});
}
toggleDarkMode(): void {
this._mode.update((mode) => (mode === 'light' ? 'dark' : 'light'));
}
setDarkMode(enabled: boolean): void {
this._mode.set(enabled ? 'dark' : 'light');
}
private _applyDarkModeClass(enabled: boolean): void {
if (enabled) {
this._renderer.addClass(this._document.body, this.DARK_MODE_CLASS);
} else {
this._renderer.removeClass(this._document.body, this.DARK_MODE_CLASS);
}
}
}
Jeśli wolisz nie polegać na domyślnych wyborach kolorów Angular Material w trybie ciemnym, możesz stworzyć osobne motywy, definiując właściwość theme-type w mapie kolorów i stosując je według potrzeb.
@use “@angular/material” as mat;
@use “./light-theme” as light-theme;
@use “./dark-theme” as dark-theme;
@mixin apply-light-theme {
@include mat.theme(
(
color: (
primary: light-theme.$primary-palette,
tertiary: light-theme.$tertiary-palette,
theme-type: light
),
typography: Poppins,
density: 0,
)
);
}
@mixin apply-dark-theme {
@include mat.theme(
(
color: (
primary: dark-theme.$primary-palette,
tertiary: dark-theme.$tertiary-palette,
theme-type: dark
),
typography: Poppins,
density: 0,
)
);
}
Tokeny systemowe
Jak wspomniano wcześniej, implementacja Material 3 opiera się na tokenach, zaimplementowanych jako zmienne CSS, aby zapewnić granularne i elastyczne stylowanie. Mixin theme generuje znaczną liczbę tokenów. Możesz je sprawdzić, otwierając narzędzia deweloperskie w przeglądarce. Aby były bardziej rozpoznawalne, wszystkie zaczynają się od prefiksu „mat-sys”. Opiszmy je pokrótce.
Kolory
Mixin theme generuje sporo tokenów dla kolorów, takich jak:
- –mat-sys-primary jako najczęściej używany kolor przez komponenty
- –mat-sys-surface jako kolor tła
- –mat-sys-error do zaalarmowania użytkownika
- –mat-sys-outline dla obramowań i separatorów
- kilka wariantów alternatywnych
- kilka odcieni powierzchni tła
Możesz przejrzeć je w dokumentacji. Ogólna zasada jest taka, aby zastosować wybrany kolor do elementu i użyć odpowiadającego tokenu „mat-sys-on” dla tekstu, ikon oraz innych elementów wizualnych, aby zapewnić odpowiednią dostępność i czytelność. Na przykład: przycisk używa –mat-sys-primary jako koloru tła i –mat-sys-on-primary jako koloru tekstu na nim.
Typografia
Material Design definuje pięć kategorii typów fontów:
- Body – Te style są używane do dłuższych fragmentów tekstu. Unikaj ekspresyjnych lub dekoracyjnych czcionek w tekście głównym, ponieważ mogą być trudniejsze do odczytania w mniejszych rozmiarach.
- Display – Jako największy tekst na ekranie, style display są zarezerwowane dla krótkich, ważnych tekstów, szczególnie na dużych ekranach. W przypadku tekstu display warto rozważyć użycie bardziej ekspresyjnej czcionki, np. stylów odręcznych lub skryptowych.
- Headline – Najlepiej nadają się do krótkich, wyróżniających się tekstów na mniejszych ekranach. Style headline są idealne do oznaczania głównych fragmentów tekstu lub kluczowych obszarów treści.
- Label – Style label są mniejsze i mają charakter użytkowy. Są używane do tekstów wewnątrz komponentów lub bardzo małych tekstów w treści, takich jak podpisy. Na przykład przyciski zazwyczaj używają stylu „label large”.
- Title – Mniejsze niż style headline, style title są używane do tekstów o średnim wyróżnieniu, które są stosunkowo krótkie. Są idealne do dzielenia drugoplanowych fragmentów tekstu lub drugorzędnych obszarów treści.
Każda kategoria fontów obejmuje trzy rozmiary: small, medium i large. Daje to w sumie 15 konfiguracji fontów, do których można uzyskać dostęp za pomocą tokenu –mat-sys-{{kategoria}}-{{rozmiar}}. Dodatkowo, konkretne części definicji fontu można uzyskać indywidualnie, dodając sufiksy, takie jak: “font”, “line-height”, “size”, “tracking” lub “weight”.
--mat-sys-body-medium: 400 0.875rem / 1.25rem Roboto, sans-serif;
--mat-sys-body-medium-font: Roboto, sans-serif;
--mat-sys-body-medium-line-height: 1.25rem;
--mat-sys-body-medium-size: 0.875rem;
--mat-sys-body-medium-tracking: 0.016rem;
--mat-sys-body-medium-weight: 400;
Podwyższenie
Podwyższenie (elevation) ma na celu zapewnienie poczucia głębi oraz organizację elementów w interfejsie:
- umożliwia powierzchniom poruszanie się przed i za innymi powierzchniami,
- odzwierciedla relacje przestrzenne,
- skupia uwagę na najwyższym poziomie
Do tej pory można było wprowadzić ten efekt za pomocą klasy “mat-elevation-z”. Nowa implementacja definiuje sześć poziomów jako tokeny od –mat-sys-level0 do –mat-sys-level5, które są zdefiniowane jako style box-shadow w CSS.
Stosowanie tokenów systemowych
Jeśli używasz zmiennych CSS, wiesz, jak proste i elastyczne jest ich użycie. Możesz je zastosować w dowolnym miejscu i łatwo stylować aplikację.
body {
background-color: var(--mat-sys-surface);
color: var(--mat-sys-on-surface);
}
h1 {
font: var(--mat-sys-headline-large);
}
h2 {
font: var(--mat-sys-headline-medium);
}
Personalizacja tokenów
Komponenty Angular Material obsługują ukierunkowaną personalizację określonych tokenów za pomocą mixinów overrides. Pozwala to na precyzyjne modyfikowanie zmiennych motywu na poziomie systemowym, a także na poziomie poszczególnych komponentów.
To API zapewnia, że modyfikowane tokeny są poprawnie zapisane, zapewniając walidację i kompatybilność wsteczną, jeśli tokeny zostaną dodane, przeniesione lub zmienione w przyszłych wersjach Angular Material.
Angular zdecydowanie odradza i nie wspiera bezpośredniego nadpisywania CSS komponentów poza API. Struktura DOM i klasy CSS komponentów są uważane za prywatne szczegóły implementacyjne, które mogą ulec zmianie bez uprzedzenia. Zamiast tego zmienne CSS używane przez komponenty powinny być definiowane i modyfikowane za pomocą API overrides.
Tokeny systemowe
Jeśli potrzebujesz indywidualnie dostosować którykolwiek z opisanych powyżej tokenów, możesz zmienić jego wartość za pomocą mixinu theme-overrides:
@use “@angular/material” as mat;
@use “./theme-colors” as theme;
html {
@include mat.theme(
(
color: theme.$primary-palette,
typography: Poppins,
density: 0,
)
);
}
.dark-container {
@include mat.theme-overrides((
primary-container: #001e2c,
on-primary-container: #dbe3eb
));
}
lub bezpośrednio w mixinie theme:
@use “@angular/material” as mat;
@use “./theme-colors” as theme;
html {
@include mat.theme(
(
color: theme.$primary-palette,
typography: Poppins,
density: 0,
), $overrides: (
primary-container: #001e2c,
)
);
}
Tokeny komponentów
Każdy komponent Angular Material zawiera mixin overrides, który umożliwia modyfikowanie tokenów definiujących m.in. kolory, typografię i gęstość. Szczegółowe informacje na temat API overrides dla każdego komponentu, w tym lista dostępnych tokenów, które można modyfikować, znajdują się na odpowiedniej stronie dokumentacji w zakładce Styling.
@use “@angular/material” as mat;
:root {
@include mat.dialog-overrides((
content-padding: 3rem
))
}
.uppercase-button {
@include mat.button-overrides(
(
filled-label-text-transform: uppercase,
outlined-label-text-transform: uppercase,
protected-label-text-transform: uppercase,
text-label-text-transform: uppercase,
)
)
}
Podsumowanie
To kolejna istotna zmiana w Angular Material, ale uważam, że przejście w kierunku opisywania designu za pomocą tokenów to fantastyczne podejście. Zapewnia ono granularną kontrolę nad wyglądem aplikacji i oferuje niezwykłą elastyczność. Mam nadzieję, że ten artykuł pomoże Ci płynnie dostosować się do tych zmian lub zbudować atrakcyjny wizualnie design od podstaw.