Help us understand the problem. What is going on with this article?

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

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

Lightning Design System の使用

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

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

また、デザイントークンを使用することで、値のハードコーディングを避けることができます。先頭に --lwc- を付与し、リンク先のドキュメントでケバブケースで表記されているトークンを、キャメルケースに置き換えればOKです。

designTokenExample.css
.error {
    background-color: var(--lwc-colorBackgroundNotificationNew);
    color: var(--lwc-colorTextError);
}
designTokenExample.html
<template>
    <span class="error">この文字は赤くなり背景は薄いグレーになります。</span>
</template>

ちなみに、Spring '20 から Aura で定義したカスタムのデザイントークンが使用できるようになるようです。

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

コンポーネント毎に、同じ名称の 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 で結果を確認)

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で結果を確認)

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 (式) を用いて条件つきスタイルを定義することができました。

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>
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 コンポーネントの場合は以下のように表現できました。

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>
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>
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 からスタイルを追加・削除・切替します。Aura では、aura:idcomponent.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 操作に分があるように思います。

ボタンをクリックした際にメッセージの表示を切り替える簡単な例で、書き方の違いを再確認しましょう。

Base Component の内部に対するスタイルの適用

Base Component の中身のスタイルを変更したいシーンがあります。例えば、<lightning-textarea> の高さを変更したい場合。

<template>
    <div>
        <h2 class="header">テキストエリアの高さを変えたい</h2>
        <lightning-textarea name="input1" label="テキストを入力" class="mytextarea">
        </lightning-textarea>
    </div>
</template>
.mytextarea textarea {
    min-height: 300px;
}

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

dom.png

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

残念ながら、現状これに対するスマートな解決策はありませんが、実装可能な方法には以下があります。

対応方法1: Base Component をカスタムで再定義する

先のドキュメントに基づいて、Base Component を自ら作り直せば、好きなようにスタイルを設定できます。しかし、この方法でコンポーネントを様々なコンテキストで再利用するには、動的なスタイルの実装が増え煩雑になってしまうでしょう。

yourOwnLightningTextarea.html
<div class="slds-form-element">
    <label class="slds-form-element__label" for="textarea-id-01">Textarea Label</label>
    <div class="slds-form-element__control">
        <textarea id="textarea-id-01" class="slds-textarea your_own_class">
        </textarea>
    </div>
</div>

対応方法2: style タグを差し込む

これは Base Component に限らない方法で、比較的簡易に実装できますが、標準のスタイルも上書きできてしまうため注意が必要です。

import { LightningElement } from 'lwc';

export default class YourComponent extends LightningElement {
    hasRendered = false;

    renderedCallback() {
        if (this.hasRendered) return;
        this.hasRendered = true;

        const style = document.createElement('style');
        style.innerText = 
            `c-your-component lightning-textarea textarea {
                 min-height: 300px;
            }`;
        this.template.querySelector('.mytextarea').appendChild(style);
    }
}

対応方法3: 静的リソースの CSS を読み込んでスタイルを上書きする

ツリーを見ると、Salesforce Platform 上では LWC はネイティブの Shodow DOM を使っていないことがわかります。Synthetic Shadow と呼ばれる Polyfill が使用されており、ここではグローバルな CSS によるスタイルの上書きが可能です。この方法でも予期しないスタイルの上書きには注意が必要です。

baseComponentCssOverrideExample.js
import { LightningElement } from 'lwc';
import customStyle from '@salesforce/resourceUrl/your_custom_css';

export default class BaseComponentCssOverrideExample extends LightningElement {
    connectedCallback() {
        loadStyle(this, customStyle)
        .then(() => {});
    }
}
your_custom_css(静的リソース)
.mytextarea textarea {
    min-height: 300px;
}

ちなみに、Shadow DOM の外側からスタイルを定義する方法として CSS Shadow Parts が W3C の Working Draft としてあり、主要なブラウザでサポートが進んでいます。「LWCのゴールは、LWCではなくなること」 とのアナウンスもあり、遅かれ早かれ Salesforce Platform 上の LWC でもネイティブの Shadow DOM が使用されるようになり、この仕組みが利用できるようになることを期待しています。

参考リンク

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした