一緒に働くデザイナーのために書いた記事です。Angular で作ったアプリのスタイルの書き方をチュートリアル形式で学んでいきます。
Angular では スタイルの記述方式を選択・変更する事ができます。今回は SCSS を選択したのでファイルの拡張子が .scss となっています。
セットアップ
チュートリアルに合わせて node のバージョンを変更します。
node のバージョンマネージャー nvm を使っているなら下記コマンドでバージョンを変更できます。
$ nvm install v12.13.0
$ nvm use v12.13.0
$ node -v
> v12.13.0 # OK!
チュートリアル用の GitHub リポジトリをローカルに持ってきます。
$ git@github.com:ringtail003/handson-angular-css.git
$ cd handson-angular-css
パッケージをインストールして Angular アプリを起動します。
# パッケージのインストール
$ npm ci
# Angularアプリの起動
$ npm start
# ブラウザで開く
$ open http://localhost:4200
こんな画面が表示されればチュートリアルを始める準備完了です。

チュートリアルの前に基礎知識
Angular のフロントエンド開発では、いくつものパーツを作りそれを組み合わせてひとつのページを構成します。それぞれのパーツは「コンポーネント」と呼び、コンポーネントごとに分割されたフォルダ構造をしています。
セットアップしたチュートリアルのフォルダをエディタで開いてみてください。src/app/components
フォルダにいくつかのコンポーネントが存在していますね。これらはフォルダ名と同じタグで HTML にマークアップする事ができます。

チュートリアル用に用意したアプリでは、これらのコンポーネントを使ってブラウザに下記のようなタグが展開されます。
<tree>
<twig>
<leaf1></leaf1>
<leaf2></leaf2>
<twig>
<twig>
<leaf1></leaf1>
<leaf2></leaf2>
<twig>
</tree>
階層関係を示すために tree(木) / twig(枝) / leaf(葉) というコンポーネントの名前にしてみました。この木には2本の枝があって、それぞれ2枚の葉っぱがあります。
└── tree
├── twig
│ ├── leaf1
│ └── leaf2
├── twig
│ ├── leaf1
│ └── leaf2
エディタに戻って src/app/components
にあるフォルダをどれか開いてみてください。 html
scss
spec.ts
ts
のというファイルが並んでいますね。これがひとつのコンポーネントを構成する基本のファイルです。これらのファイルのうち .scss
がデザイナーの領域です。

またチュートリアルを進める上で 青枠で囲ったファイル を編集する事になりますので、迷子になったらこの図を見に来てください。

さあ始めよう
ここまで問題ないでしょうか?
エラーでアプリが起動しないとか困った事があれば、右隣に座っているやさしそうなエンジニアに聞いてみてください。
それではチュートリアル行ってみましょう!
15分程度で終わるはずです。
チュートリアル完了後のソースはこちらです。うまく動かない時、参考にしてください。
https://github.com/ringtail003/handson-angular-css/tree/handson-goal
1) コンポーネントにスタイルを設定してみよう
階層の根っこにある <tree>
コンポーネントにスタイルを設定してみましょう。
<tree> <!--ここ★-->
<twig>
<leaf1></leaf1>
<leaf2></leaf2>
<twig>
<twig>
<leaf1></leaf1>
<leaf2></leaf2>
<twig>
</tree>
ブラウザに「tree」と表示されているテキストは .html
を見ると <p>tree</p>
だという事が分かります。.scss
を下記のように編集してフォントの色を変更してみましょう。
// src/app/components/tree/tree.component.scss
+ p {
+ color: #fff;
+ }
ファイルを保存するとブラウザは勝手にリロードします。リロード後の画面では <tree>
コンポーネントのフォントの色が変わるはずです。

Note: スタイルのスコープ
ところで、この画面の HTML の構造を見てみると <tree>
コンポーネントだけでなく <twig>
や <leaf>
も <p>
タグを持っている事が分かります。どうして <tree>
以外の <p>
タグにはスタイルが適用されないのでしょうか?

理由は {コンポーネント名}.component.scss に書かれたスタイルをカプセル化し、そのコンポーネントだけに適用する という Angular の仕組みによるものです。
特定の要素にだけスタイルが当たるように、よくこんなスタイルの書き方をしますよね。
.user-profile-box {
color: blue;
}
.user {
div.profile {
.button[active] {
color: red;
}
}
}
Angular の場合は、スタイルがバッティングしないように、クラス名をあてがったり階層を指定する必要はありません。 <tree>
のスタイルを設定する時は tree.component.html
と tree.component.scss
の内容だけ気にすれば良いのです。
あちこちのコンポーネントで p { color: red; }
のように書かれていたとしても、それぞれ別のスタイルとして扱われ他のコンポーネントに影響を与えないからです。
2) コンポーネント自身のタグにスタイルを設定してみよう
さきほどの例ではコンポーネントが持っている <p>
に対してスタイルを設定しました。今度はコンポーネント自身を示すタグ( <tree>
とか <twig>
)に対してスタイルを設定してみましょう。特定のタグを display: table-cell
で並べたいなど、配置に関するものに使えるはずです。
例として階層の末端の <leaf1>
<leaf2>
にスタイルを設定してみます。
<tree>
<twig>
<leaf1></leaf1> <!--ここ★-->
<leaf2></leaf2> <!--ここ★-->
<twig>
<twig>
<leaf1></leaf1> <!--ここ★-->
<leaf2></leaf2> <!--ここ★-->
<twig>
</tree>
<leaf1>
<leaf2>
それぞれの .scss
で自分自身のタグ( <leaf1>
<leaf2>
)のスタイルを設定するには :host という疑似クラスセレクターを使用 します。
// src/app/components/leaf1/leaf1.component.scss
// <leaf1> に対するスタイル定義
+ :host {
+ display: inline-block;
+ }
// src/app/components/leaf2/leaf2.component.scss
// <leaf2> に対するスタイル定義
+ :host {
+ display: inline-block;
+ }
ブラウザの画面はこのように変わるはずです。

3) コンポーネント自身のクラスに応じてスタイルを設定してみよう
次は <leaf1 class="active">
のように、コンポーネントに与えられたクラスに対してスタイルを設定してみます。
<tree>
<twig>
<leaf1></leaf1> <!--ここ★-->
<leaf2></leaf2> <!--ここ★-->
<twig>
<twig>
<leaf1></leaf1> <!--ここ★-->
<leaf2></leaf2> <!--ここ★-->
<twig>
</tree>
クリックした時に class=""
と class="active"
が切り替わるようにしたいので、階層関係の親である <twig>
のコードをちょっと変更します。
下記のコードを何も考えずにコピペしてください。
// src/app/components/twig/twig.component.ts
export class TwigComponent implements OnInit {
+ activeLeaf = null;
constructor() { }
ngOnInit() {
}
+ activate(no: number) {
+ this.activeLeaf = no;
+ }
+ isActive(no: number) {
+ return no === this.activeLeaf;
+ }
}
// src/app/components/twig/twig.component.html
<div>
<p>twig</p>
- <leaf1></leaf1>
- <leaf2></leaf2>
+ <leaf1
+ [class.active]="isActive(1)"
+ (click)="activate(1)"
+ ></leaf1>
+ <leaf2
+ [class.active]="isActive(2)"
+ (click)="activate(2)"
+ ></leaf2>
</div>
ブラウザでエラーが出たらコピペをどこかミスしています。さっきセットアップを手伝ってくれた右隣のエンジニアに聞いてみてください。「えぇ、また?」って顔をされたら「あっ解決しました!」って答えつつ、もうひとつ右隣のさらにやさしそうなエンジニアに聞いてみてください。
コピペしたコードのおかげで画面の「leaf1」「leaf2」の枠をクリックすると class="active"
がスイッチするようになりました。では active な時のスタイルを設定しましょう。
// src/app/components/leaf1/leaf1.component.scss
+ :host(.active p) {
+ background: yellow;
+ }
// src/app/components/leaf2/leaf2.component.scss
+ :host(.active p) {
+ background: yellow;
+ }
ブラウザの画面で「leaf1」「leaf2」の枠をクリックしてみてください。クラスに応じてスタイルが変わるはずです。

4) 親コンポーネントから子コンポーネントのスタイルを操作してみよう
ここまでチュートリアルを進めてみて「leaf1 と leaf2 って結局同じスタイル書いてんじゃん、効率悪いし面倒くせ( `д´)ケッ」って思いましたよね。絶対そう思ってますよね。私にはあなたの心が見えます。
それでは今度は、配下の子コンポーネントに一斉に適用するスタイルを書いてみましょう。
<tree>
<twig> <!--ここ★-->
<leaf1></leaf1>
<leaf2></leaf2>
<twig>
<twig> <!--ここ★-->
<leaf1></leaf1>
<leaf2></leaf2>
<twig>
</tree>
<twig>
コンポーネントは <leaf1>
<leaf2>
を持っています。クリック操作によって <leaf1 class="active">
のように変化するため、クラス指定でスタイルを書いてみましょう。
// src/app/components/twig/twig.component.scss
// 自身の持っているタグのうち active クラスが付いたものに適用
+ .active {
+ color: orange;
+ }
どうですか!一斉に適用する事もできちゃうんですよ!!

5) スタイルのスコープを突破してみよう
親コンポーネントから子コンポーネントのスタイルを操作するもうひとつの方法として encapsulation
を指定するというやり方もあります。
<tree>
<twig> <!--ここ★-->
<leaf1></leaf1>
<leaf2></leaf2>
<twig>
<twig> <!--ここ★-->
<leaf1></leaf1>
<leaf2></leaf2>
<twig>
</tree>
エンジニアの領域に手を入れる事になりますが、たった2行なので勇気を振り絞ってコピペしてください。
// src/app/components/twig/twig.component.ts
- import { Component, OnInit } from '@angular/core';
+ import { Component, OnInit, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'twig',
templateUrl: './twig.component.html',
- styleUrls: ['./twig.component.scss']
+ styleUrls: ['./twig.component.scss'], // 末尾カンマを忘れずに
+ encapsulation: ViewEncapsulation.None,
})
// src/app/components/twig/twig.component.scss
+ div {
+ font-weight: bold;
+ color: gray;
+ }
ViewEncapsulation
は Angular でスタイルのカプセル化の方法を指定するものです。
-
Emulated
(デフォルト)Shadow DOM をエミュレートしカプセル化が行われます。 -
None
カプセル化を行いません。 -
Native
ShadowDom
ブラウザが提供する Shadow DOM を使用します。非対応ブラウザではエラーになります。Native は Shadow DOM 標準化前のベンダごとの実装を使用するようです。
コピペコードでは None
を指定したので、<twig>
コンポーネントに限りカプセル化は一切行われなくなりました。ブラウザを確認すると <twig>
とその配下のコンポーネントの <div>
のフォントが一斉に変わるはずです。

6) テーマに応じたスタイルを設定してみよう
デザイナーならばアプリのデザインのテーマを作ったり購入して使う事もあると思います。今度はテーマに応じてちょこっと調整するためのスタイルを設定しましょう。
<tree>
<twig>
<leaf1></leaf1> <!--ここ★-->
<leaf2></leaf2> <!--ここ★-->
<twig>
<twig>
<leaf1></leaf1> <!--ここ★-->
<leaf2></leaf2> <!--ここ★-->
<twig>
</tree>
まず架空のテーマ theme-funky
クラスをアプリのベースとなる HTML ファイルに設定しておきます。
// src/index.html
- <body>
+ <body class="theme-funky">
<app-root></app-root>
</body>
次に <leaf1>
<leaf2>
にスタイルを設定します。:host-context 疑似クラスセレクター を使うと DOM ツリーを body タグまで遡って CSS クラスやタグを検索する事ができます。
// src/app/components/leaf1/leaf1.component.scss
// 祖先要素に theme-funky クラスが存在すれば適用
+ :host-context(.theme-funky) p::before {
+ content: "🌵"
+ }
// src/app/components/leaf2/leaf2.component.scss
// 祖先要素に theme-funky クラスが存在すれば適用
+ :host-context(.theme-funky) p::before {
+ content: "🍀"
+ }
ブラウザを確認すると文字がちょっとファンキーになっているはずです。

index.html
に書いたテーマを別のものに変更すると、ファンキーさは消えます。
// src/index.html
- <body class="theme-funky">
+ <body class="theme-serious">
<app-root></app-root>
</body>
Note: テーマの切り替え
画面にドロップダウンを設置してアプリのユーザーが任意にテーマを切り替える事もあるかもしれません。その場合はソースコードで分岐したほうが楽なので、エンジニアに依頼してください。
// src/app/app.component.ts
+ import { HostBinding } from '@angular/core';
export class AppComponent {
+ @HostBinding('class') get themeClass() {
+ return userProfile.theme || null; // theme-funky | theme-serious | null
+ }
併用した場合は
@HostBinding
が優先されるため<body>
の指定はフォールバックとして利用できます。
7) アプリケーション全体のスタイルを設定してみよう
時にはアプリケーション全体で共通して使うスタイルを書くこともあるでしょう。例えばバリデーションエラーを示す class="invalid"
など、一箇所にスタイルを集約しておきたいですよね。
src
のすぐ下の styles.scss
はそのためのファイルです。ここにスタイルを追加してみましょう。
// src/styles.scss
+ div {
+ border: solid 1px gray;
+ }
ブラウザを確認すると階層に関係なく全ての <div>
に枠線が表示されるはずです。

以上でチュートリアルは終わりです!お疲れさまでした。
おわりに
チュートリアルが完了した画面を改めて見てください。サンプルとは言え、まあひどい出来のデザインですね。

私はスタイルを当てる作業が嫌いなんです。オサレなものを作ろうと思う純粋な心とはうらはらに、いつもゴミみたいな画面が出来上がってひどい自己嫌悪に陥るからです。チュートリアルを終えて Angular で CSS/SCSS をガンガン書く準備ができたデザイナーさんへ、最後にメッセージを残して締めくくりたいと思います。
