Komponen Angular bisa membawa CSS-nya sendiri lewat properti styles di decorator, dan secara default CSS itu cuma berlaku untuk komponen itu. Istilahnya: style-nya ter-enkapsulasi (encapsulated). Bayangin food court tempat gerobak Martabak Bang Jali berjualan. Tiap gerobak mengecap semua piringnya dengan stempel sendiri. Kalau Bang Jali bilang "piring yang bercap Jali dicat oranye", piring gerobak sebelah gak ikut berubah, walaupun sama-sama piring. Angular melakukan persis itu: tiap elemen di template sebuah komponen diberi cap, dan tiap aturan CSS komponen itu ditulis ulang supaya cuma mengenai elemen yang bercap sama.
🧪 Tebakan cepat: MenuItem punya
stylesberisip { color: #c2410c; }(oranye). Template App juga punya paragraf sendiri,<p>Buka jam 16.00</p>, di atas kartu-kartu MenuItem. Paragraf milik App itu ikut oranye? Jawabannya ada di bagian "Di balik layar: cap _ngcontent".
styles: CSS milik komponen
import { Component, Input } from '@angular/core'
@Component({
selector: 'app-menu-item',
standalone: true,
template: `
<h3>{{ nama }}</h3>
<p>Stok: {{ stok }}</p>
`,
styles: `
:host { display: block; margin-top: 8px; padding: 10px 12px; border: 1px solid #ddd; border-radius: 12px; }
:host(.habis) { opacity: 0.5; }
:host-context(.gelap) h3 { color: #fbbf24; }
p { color: #c2410c; }
`,
})
export class MenuItem {
@Input() nama = ''
@Input() stok = 0
}
@Component({
selector: 'app-root',
standalone: true,
imports: [MenuItem],
template: `
<main [class.gelap]="gelap">
<h1>Martabak Bang Jali</h1>
<p>Buka jam 16.00</p>
<button (click)="gelap = !gelap">Mode gelap</button>
<app-menu-item nama="Martabak Telur Spesial" [stok]="6" />
<app-menu-item nama="Terang Bulan Pandan" [stok]="0" [class.habis]="true" />
</main>
`,
styles: `
main { padding: 12px; border-radius: 12px; }
.gelap { background: #1f2937; color: #f9fafb; }
`,
})
export class App {
gelap = false
}
stylesberisi CSS biasa. Sejak Angular 17 boleh satu string seperti di atas; bentuk lama berupa array,styles: [`p { color: blue; }`], masih jalan dan dipakai di latihan.- Di proyek CLI, CSS-nya biasanya di file sendiri:
styleUrl: './menu-item.css'. Playground gak bisa membaca file terpisah, jadi di sini pakaistyles. - Empat aturan MenuItem dibahas satu per satu di bawah. Yang pertama dicoba:
p { color: #c2410c; }.
Di balik layar: cap _ngcontent
Buka DevTools di Playground, tab Elements. Ini yang dibuat Angular 22 (versi Playground) dari kode di atas (atribut ng-version dibuang supaya ringkas):
<app-root _nghost-ng-c2838053730="">
<main _ngcontent-ng-c2838053730="">
<p _ngcontent-ng-c2838053730="">Buka jam 16.00</p>
<app-menu-item _ngcontent-ng-c2838053730="" _nghost-ng-c3691344558="">
<h3 _ngcontent-ng-c3691344558="">Martabak Telur Spesial</h3>
<p _ngcontent-ng-c3691344558="">Stok: 6</p>
</app-menu-item>
Dan ini CSS MenuItem yang Angular tulis ke <head>:
p[_ngcontent-ng-c3691344558] { color: #c2410c; }
- Tiap komponen dapat id sendiri: App
ng-c2838053730, MenuItemng-c3691344558. Angkanya dibuat Angular dari bentuk komponennya, jadi di kodemu bisa beda. - Tiap elemen di template sebuah komponen dapat cap
_ngcontent-<id>milik komponen itu. Elemen host-nya (<app-menu-item>) dapat_nghost-<id>. - Tiap selector ditulis ulang supaya mensyaratkan cap itu.
pjadip[_ngcontent-ng-c3691344558]. <p>Buka jam 16.00</p>membawa cap App, bukan cap MenuItem, jadi aturan itu gak mengenainya. Paragraf App tetap hitam. Itu jawaban tebakannya.
Mode ini namanya ViewEncapsulation.Emulated, default-nya Angular: meniru Shadow DOM tanpa Shadow DOM sungguhan. Di proyek CLI, penulisan ulangnya terjadi saat build: hasil ng build menyimpan p[_ngcontent-%COMP%]{color:#c2410c}, dan %COMP% diganti id komponennya saat aplikasi jalan. Komponen tanpa styles sama sekali gak diberi cap.
:host dan :host-context
Elemen <app-menu-item> ditulis di template App, jadi selector app-menu-item { ... } di dalam MenuItem ditulis ulang jadi app-menu-item[_ngcontent-ng-c3691344558] dan gak pernah cocok. Untuk elemen host, pakai :host:
| Ditulis di MenuItem | Ditulis ulang Angular jadi | Artinya |
|---|---|---|
:host { display: block; ... } |
[_nghost-ng-c3691344558] { ... } |
elemen <app-menu-item> itu sendiri |
:host(.habis) { opacity: 0.5; } |
.habis[_nghost-ng-c3691344558] { ... } |
host yang punya class habis |
:host-context(.gelap) h3 ditulis ulang jadi dua selector sekaligus: .gelap[_nghost-ng-c3691344558] h3[_ngcontent-ng-c3691344558] (host-nya sendiri punya class gelap) dan .gelap [_nghost-ng-c3691344558] h3[_ngcontent-ng-c3691344558] (salah satu leluhurnya yang punya).
- Elemen buatan Angular itu
display: inlinekalau gak diatur, jadi bingkai dan padding baru rapi setelah:host { display: block; }. - App yang memutuskan class-nya (
[class.habis]="true"di tag kartu), MenuItem yang memutuskan tampilannya (:host(.habis)). - Klik Mode gelap:
<main>dapat classgelap, dan judul kedua kartu jadi kuning.
Binding style dan class
Untuk nilai yang dihitung, ikat langsung di template:
<p [style.color]="warnaStok(m.stok)">Sisa {{ m.stok }}</p>
<app-menu-item [nama]="m.nama" [stok]="m.stok" [class.habis]="m.stok === 0" />
[style.color]="..."mengisi satu properti CSS dengan hasil ekspresinya. KalauwarnaStok()mengembalikan'green', DOM-nya jadi<p style="color: green;">. Ada juga bentuk dengan satuan:[style.width.%]="persen".[class.habis]="kondisi"memasang classhabissaat kondisinyatruedan melepasnya saatfalse.
ViewEncapsulation.None dan ShadowDom
import { ViewEncapsulation } from '@angular/core'
@Component({
selector: 'app-menu-item',
standalone: true,
encapsulation: ViewEncapsulation.None,
// template dan styles sama seperti tadi
})
encapsulation |
CSS MenuItem | Buka jam 16.00 (App) | Bingkai kartu (:host) |
|---|---|---|---|
Emulated (default) |
ditulis ulang dengan cap | hitam | ada |
None |
masuk <head> apa adanya, berlaku global |
oranye | hilang |
ShadowDom |
di dalam shadow root tiap kartu | hitam | ada |
- None mematikan cap.
p { color: #c2410c; }jadi aturan untuk seluruh halaman, jadi paragraf App ikut oranye: style-nya bocor.:hostgak dikenali di luar Shadow DOM, jadi bingkai kartu hilang. - ShadowDom memakai Shadow DOM sungguhan. Aturan dari luar gak bisa masuk; cuma properti yang diwariskan, seperti warna teks, yang menembus.
📌 ::ng-deep, cara lama yang dianggap usang
/* styles App */
:host ::ng-deep h3 { text-decoration: underline; }
Angular menulisnya ulang jadi [_nghost-ng-c2838053730] h3, tanpa cap di h3, jadi judul di dalam MenuItem ikut bergaris bawah. ::ng-deep sudah dianggap usang (deprecated) dan cuma dipertahankan untuk kode lama. Tanpa :host di depannya, ::ng-deep h3 malah jadi aturan global untuk seluruh halaman. Kalau App memang perlu mengubah tampilan MenuItem, minta MenuItem menyediakan @Input() atau CSS variable, misalnya color: var(--warna-judul, inherit).
⚠️ Jebakan yang sering ketemu
- Menulis tag komponen sendiri di styles-nya.
app-menu-item { border: ... }di dalam MenuItem gak mengenai apa pun. Pakai:host. ViewEncapsulation.Noneuntuk "membereskan" satu aturan. CSS komponen itu bocor ke seluruh aplikasi. Gaya global tempatnya disrc/styles.css.::ng-deeptanpa:host. Aturannya berlaku untuk seluruh halaman, bukan cuma di bawah komponen itu.- Detail latihan. Tambahkan
styles: [`p { color: blue; }`]di decorator dan biarkan template<p>Teks berwarna biru</p>dari starter. Penilai membaca warna yang benar-benar dipakai browser,rgb(0, 0, 255), dan mencari katastylesdi kodemu, jadi<p style="color: blue">lolos soal warna tapi tetap gagal.
TL;DR: CSS di styles sebuah komponen ter-enkapsulasi (encapsulated): dengan ViewEncapsulation.Emulated (default), Angular memberi cap _ngcontent-<id> ke elemen template-nya dan menulis ulang p jadi p[_ngcontent-<id>], jadi paragraf komponen lain gak ikut berubah. :host mengatur elemen host-nya sendiri (:host(.habis) untuk host dengan class tertentu), dan :host-context(.gelap) bereaksi ke class di leluhurnya. [style.color]="..." dan [class.habis]="..." mengikat style dan class ke ekspresi. ViewEncapsulation.None membuat CSS-nya global dan bocor, ShadowDom memakai Shadow DOM sungguhan, dan ::ng-deep sudah usang.