Styling Angular — Angular

View Encapsulation membuat style komponen terisolasi secara default. Scoped Style (Default) @Component({ template: `Teks merah`, styles: [`p { color: red…

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 styles berisi p { 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
}

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; }

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).

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" />

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

📌 ::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

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.

Yang akan kamu pelajari