Lightning Web Components における CSS の基礎と応用

Lightning Web Components (以下、LWC) に対するスタイルの設定方法を、Aura コンポーネントの場合との比較を交えつつ、様々な例を挙げて紹介します。なお、この記事では Salesforce Platform 上 の LWC の開発を前提としています。

## Lightning Design System の使用
Salesforce 標準のデザインに沿ったコンポーネントを作成するのであれば、[Lightning Design System (LDS)](https://www.lightningdesignsystem.com/) を使用しましょう。カスタムの LWC は Aura と同じく自動的に LDS が適用されますので、LDS の CSS クラスを必要に応じて用いることができます。もちろん、自分でゼロから作り始める前に、[Base Component](https://developer.salesforce.com/docs/component-library/overview/components) を組み合わせられないかは先に確認しましょう。

```ldsExample.html
<template>
    <!-- 例) slds-p-horizontal_small は左右にパディングを取る。
    https://www.lightningdesignsystem.com/utilities/padding/ -->
    <div class="slds-p-horizontal_small">
         Hello!
    </div>
</template>
```

また、[デザイントークン](https://www.lightningdesignsystem.com/design-tokens/#category-text-color)を使用することで、SLDS で定義された値をハードコーディングせずに使用することができます。先頭に `--lwc-` を付与し、リンク先のドキュメントでケバブケースで表記されているトークンを、キャメルケースに置き換えればOKです。

```designTokenExample.css
.error {
    background-color: var(--lwc-colorBackgroundNotificationNew);
    color: var(--lwc-colorTextError);
}
```

```designTokenExample.html
<template>
    <span class="error">この文字は赤くなり背景は薄いグレーになります。</span>
</template>
```

独自のデザイントークンを定義することも可能です。以下のような `.css` ファイルと、 `.js-meta.xml` ファイルだけを含むコンポーネントを作成し、

```branding.css
:host {
    --text-primary: #000000;
    --text-secondary: #333333;
}
```

これを利用するコンポーネント側は、以下のように CSS をインポートして使用することができます。

```exampleComponent.css
@import 'c/branding';

p {
    color: var(--text-secondary);
}
```

## カスタムのスタイルを適用

コンポーネント毎に、同じ名称の CSS ファイルを1つだけ含めることができます。その CSS ファイルで定義されたスタイルは、そのコンポーネントに対してのみ適用されます。Aura と異なり `THIS` CSS クラスを定義する必要はありません。

```hello.html
<template>
    <h1>Hello LWC!</h1>
</template>
```

```hello.css
h1 {
    font-size: 2rem;
    color: rgb(0, 112, 210);
}
```

親コンポーネントで定義されたスタイルは、子コンポーネントには継承されません。(→ [Playground で結果を確認](https://developer.salesforce.com/docs/component-library/tools/playground/4gMKmf9u))

```hello.html
<template>
    <h1>Hello LWC!</h1>
    <c-child></c-child>
</template>
```

```child.html
<template>
    <h1>Hello Child!</h1>
    <p>これは子コンポーネントです。</p>
</template>
```

```hello.css
/* child の h1 はそのまま */
h1 {
    font-size: 2rem;
    color: rgb(0, 112, 210);
}
```

ここで、`c-child` に外枠を付けるにはどうしたらいいでしょうか。

```hello.css
h1 {
    color: rgb(0, 112, 210);
}

/* よくない例 */
c-child {
    display: block;
    border: 1px solid rgb(200, 200, 200);
}
```
上記のように、親の CSS に `c-child` セレクタを定義することもできますが、親のスタイル定義に子のスタイル定義を混ぜてしまうと、見通しが悪くなりメンテナンス性にも欠けます。子コンポーネントのスタイルは、子コンポーネント側で定義しましょう。`:host()` 疑似クラスを用いると、そのコンポーネント自身(ホスト要素) にアクセスすることができます。

```child.css
:host {
    display: block;
    border: 1px solid rgb(200, 200, 200);
}
```

下記のように、カッコとセレクタを使って、ホストのスタイルを状態に応じて変えることもできます。(→ [Playgroundで結果を確認](https://developer.salesforce.com/docs/component-library/tools/playground/OJxQWvw1/5))

```child.css
:host {
    display: block;
    border: 1px solid rgb(200, 200, 200);
}

:host(.active) {
    border-width: 3px;
}
```

```hello.html
<template>
    <div class="app">
        <h1>Hello LWC!</h1>
        <c-child></c-child>
        <c-child class="active"></c-child> <!--これだけ枠が太くなる-->
        <c-child></c-child>
    </div>
</template>
```

## 動的なスタイルの適用
例えば、チェックボックスにチェックを入れたら文字を薄くする、ボタンを押したら特定の要素を非表示にするなど、スタイルを動的に切り替えるにはいくつかの方法があります。
### クラスのバインディング
まずはクラスを可変にする方法です。Aura では、マークアップ内に [Expression (式)](https://developer.salesforce.com/docs/atlas.ja-jp.lightning.meta/lightning/expr_overview.htm) を用いて条件つきスタイルを定義することができました。

```html:AuraDynamicStyleExample.cmp
<aura:component>
    <div class="{!v.record.isActive__c ? 'slds-section slds-is-open' : 'slds-section'}">
        <!--中略-->
    </div>
</aura:component>
```

LWC では、Aura のように式の中で演算子が利用できないため、JavaScript 側にロジックを記載する必要があります。

```lwcDynamicStyleExample.html
<template>
    <div class={sectionClass}>
        <!--中略-->
    </div>
</template>
```

```javascript:lwcDynamicStyleExample.js(※説明に必要な箇所のみ抜粋)
get sectionClass() { //getter を定義
    return this.record.isActive__c ? 'slds-section slds-is-open' : 'slds-section';
}
```

JavaScript に直接 CSS クラス名を書きたくない場合は、`<template if:true|false={condition}>` で要素を切り替えることもできます。

```lwcDynamicStyleExample.html
<template>
    <template if:true={record.isActive__c}> <!-- boolean を返す getter または property を用いる-->
        <div class="slds-section slds-is-open">
            <!--中略-->
        </div>
    </template>
    <template if:false={record.isActive__c}>
        <div class="slds-section">
            <!--中略-->
        </div>
    </template>
</template>
```

Aura に慣れていると、書きづらい、不便だと感じるかもしれませんが、LWC ではマークアップとロジックが必ず分離されるため安全だという見方ができます。

### ループ内での動的なスタイル
ループ内の場合はもう少し工夫が必要です。取引先責任者の一覧のうち、条件に合致するレコードだけスタイルを変えたい、など。Aura コンポーネントの場合は以下のように表現できました。

```html:auraIterationDynamicStyleExample.cmp
<aura:component>
    <aura:attribute type="Contact[]" name="contacts" />
    <aura:iteration items="{!v.contacts}" var="contact">
       <div class="{!contact.isActive__c ? 'slds-section slds-is-open' : 'slds-section'}">...</div>
    </aura:iteration>
</aura:component>
```

#### 対応方法1: ループ内要素を子コンポーネント化し、ループアイテムを受けとる
スタイルやロジックは子コンポーネントで定義します。

```parent.html
<template>
    <template for:each={contacts} for:item="contact">
         <c-child contact={contact} key={contact.Id}></c-child>
    </template>
</template>
```

```child.html
<template>
    <div class={sectionClass}>...</div>
</template>
```

```javascript:child.js(※説明に必要な箇所のみ抜粋)
@api contact;

get sectionClass() {
    return this.contact.isActive__c ? 'slds-section slds-is-open' : 'slds-section';
}
```

#### 対応方法2: ループアイテムにプロパティを追加する

1コンポーネントで済ませたいのであればこちら。`@wire` を使用している場合は、Property ではなく、Function として Apex を呼び出し、結果を加工すると良いでしょう。

```parent.html
<template>
    <template for:each={contacts} for:item="contact">
         <div class={contact.styleClass} key={contact.record.Id}>{contact.record.Name}</div>
    </template>
</template>
```

```javascript:parent.js(※説明に必要な箇所のみ抜粋)
@wire(getContacts)
wiredContacts({ error, data}) {
    if (error) {
     // エラー処理
    } else if (data) {
     this.contacts = data.map(contact => {
           return {
              record: contact,
              styleClass: contact.isActive__c ? 'slds-section slds-is-open' : 'slds-section'
           }
       })
   }
}
```
### 実行時のスタイルの追加と削除

DOM を操作してスタイルを制御することもできます。`template.querySelector()` を用いて要素を特定し、[classList](https://developer.mozilla.org/ja/docs/Web/API/Element/classList) からスタイルを追加・削除・切替します。Aura では、`aura:id` と `component.find()` でコンポーネントを特定して、 `$A.util.addClass()` や `$A.util.removeClass()` でクラスを制御しました。`template.querySelector()` では id 属性によるクエリは非推奨のため注意が必要です。

```example.html
<template>
   <div id="first">First</div>
   <div class="unique-class-name">Second</div>
</template>
```

```example.js
import { LightningElement } from 'lwc';

export default class Example extends LightningElement {
    demoQuerySelector() {
        //querySelector の例
        this.template.querySelector('div'); // OK. <div>First</div>
        this.template.querySelectorAll('div'); // OK. [<div>First</div>, <div>Second</div>]
        this.template.querySelector('#first'); // NG

        const secondDiv = this.template.querySelector('.unique-class-name'); // OK. <div>Second</div>
        //スタイルを追加する例
        secondDiv.classList.add('slds-is-open');
    }
}
```

### 比較
クラスのバインディング (式を使用したスタイルの適用) はコンポーネントの状態を利用できますが、実行時のスタイルの追加・削除 (DOM 操作) にはそれがありません。しかし、規模が大きい場合はパフォーマンスの観点で DOM 操作に分があるように思います。

[ボタンをクリックした際にメッセージの表示を切り替える簡単な例](https://developer.salesforce.com/docs/component-library/tools/playground/gPaEBXME/19)で、書き方の違いを再確認しましょう。

## Base Component の内部に対するスタイルの適用 (Styling Hook)
Base Component の中身のスタイルを変更したいシーンがあります。例えば、`<lightning-textarea>` の高さを変更したい場合。

```html
<template>
    <div>
        <h2 class="header">テキストエリアの高さを変えたい</h2>
        <lightning-textarea name="input1" label="テキストを入力" class="mytextarea">
        </lightning-textarea>
    </div>
</template>
```

```css
.mytextarea textarea {
    min-height: 300px;
}
```

上記の例では、テキストボックス自体の高さは指定した値になりませんが、これは期待通りの結果です。

![dom.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184624/35e7f348-9505-57e0-432b-dda75a3ac039.png)

`<lightning-textarea>` の実装は [Lightning Design System のドキュメント](https://www.lightningdesignsystem.com/components/textarea/) の通りですが、前述の通り、親コンポーネントで定義したスタイルは、子コンポーネントである `<lightning-textarea>` 内の要素には適用されません。

Base Component のスタイルを更新するためのカスタムプロパティを提供する、Styling Hook と呼ばれる仕組みが Spring' 22 で GA になりました。`lightning-textarea` の高さを変更する場合は、[LDS の Textarea の Styling Hook](https://www.lightningdesignsystem.com/components/textarea/#Styling-Hooks-Overview) に従い、以下の CSS で実現できます。

```css
:host {
    --slds-c-textarea-sizing-min-height: 300px;
}
```

# 参考リンク
* [Google Web Fundamentals | Shadow DOM v1: 自己完結型ウェブ コンポーネント](https://developers.google.com/web/fundamentals/web-components/shadowdom#styling) 
* [Lightning Web Component Developer Guide | CSS](https://developer.salesforce.com/docs/component-library/documentation/lwc/lwc.create_components_css_intro)
* [Trailhead | Lightning Web コンポーネントへのスタイルとデータの追加](https://trailhead.salesforce.com/ja/content/learn/modules/lightning-web-components-basics/add-styles-and-data-to-a-lightning-web-component)
* [Lightning Design System スタイル設定フックを使用したコンポーネントのカスタマイズ (正式リリース)](https://help.salesforce.com/s/articleView?id=release-notes.rn_slds_styling_hooks.htm&type=5&release=238)