Twilio Flex の見た目を React Plugin でカスタマイズしてみる

Twilio Flex の見た目を React Plugin でカスタマイズしてみる

Twilio Flex の React 製コンタクトセンター UI を、ローカル dev サーバー上の Plugin で配色・ヘッダーブランド・文言・着信音までカスタマイズします。本番反映までのライフサイクルも概念ベースで紹介します。
2026.04.30

はじめに

Twilio Flex は、Twilio が提供するコンタクトセンタープラットフォームです。エージェント向けの UI が React で作られていて、React エンジニアであれば自分で書いたコンポーネントを差し込んで見た目や挙動をカスタマイズできます。

ui customize

本記事では、Flex を初めて触る React 開発者を対象に、ローカルの開発サーバー上で次の 4 つを試します。

  • 配色テーマを書き換える
  • ヘッダーに自前のブランドテキストを差し込む
  • UI 上の英語文言を日本語に置き換える
  • 着信時に Web Audio API でビープ音を鳴らす

着地点はローカルの dev サーバーに留めますが、記事の終盤で本番反映までのライフサイクルを概念ベースで紹介します。実際にコールセンターを運用する場面で、開発から本番反映までどのような流れになるのかをイメージできる構成にしています。

なお、Twilio アカウントの有効化や Flex プロジェクトの初期セットアップについては、以下の記事をご参照ください。

https://dev.classmethod.jp/articles/twilio-flex-first-call-receive/

本記事ではこの状態を前提として、Plugin 開発のセットアップから話を始めます。

Twilio Flex とは

Twilio Flex は、電話、SMS、Web チャット、メールなどのチャネルを 1 つのエージェント向け UI に集約したクラウド型のコンタクトセンタープラットフォームです。エージェントが顧客対応で使う画面が React で作られている点が特徴です。見た目や挙動を、自分で書いた JavaScript モジュール (Plugin) で拡張できます。

自前のサーバーは必要か

通常の利用では不要です。Flex UI 本体、Plugin の配信、TaskRouter (タスク振り分け)、Voice / SMS / Studio などのバックエンドサービスは、いずれも Twilio がホストする SaaS で完結します。Plugin も Twilio Plugins Service にアップロードして配信される仕組みです。

開発時に登場するローカル dev サーバーは、本番運用上のサーバーとは別物です。twilio flex:plugins:start で起動するものの正体は webpack-dev-server で、自分の Plugin の JavaScript バンドルをローカルでビルドして配信するためのものに過ぎません。ブラウザは http://localhost:3000/ を開きますが、そこから Flex UI 本体は Twilio の CDN (assets.flex.twilio.com) から読み込まれます。localhost で完結する Flex ではなく、Twilio がホストする Flex UI に自分の Plugin だけをローカルから注入している状態だと理解するのが正確です。

ユーザー側に追加のサーバー運用が発生するのは、独自のバックエンド連携 (自社 CRM への問い合わせ、独自 API の呼び出しなど) を組み込みたいときに限ります。

アーキテクチャの全体像

顧客が電話やメッセージを送ると、Twilio クラウド側の Voice / SMS / Conversations が受けます。Studio Flow (ノーコードのフロー定義ツール) が応答ロジックを担当し、TaskRouter がタスクをエージェントに振り分けます。エージェントは Flex UI でタスクを受け取ります。Flex UI には Plugins Service 経由でカスタム Plugin がロードされる、というのが全体の流れです。

対象読者

  • Twilio Flex を初めて触る React エンジニア
  • コンタクトセンター画面の見た目を Plugin でカスタマイズしたい開発者・運用者
  • ローカル開発から本番反映までの流れを概念レベルで把握しておきたい方

参考

開発環境のセットアップ

ここから手を動かす話に入ります。本検証の環境は次の通りです。

  • macOS, WebStorm
  • Node.js v22 系 (後述する制約から v22 を使う)

Node.js のバージョン

Twilio CLI の最新版である 6 系と、Flex Plugins CLI 7 系は、それぞれ Node.js のバージョンに制約があります。とくに @twilio/flex-plugin-scripts v7.1.2 は engine フィールドで ^16 || ^18 || ^20 || ^22 を要求しており、Node v24 以上の環境では npm i -g twilio-cli のあとの twilio plugins:install @twilio-labs/plugin-flexengine "node" is incompatible のエラーで失敗します。

筆者の環境は Node v24 がデフォルトでしたが、nvm 経由で v22 に切替えて作業しました。v24 系を使っている方は、nvm や fnm 等で v22 系を併用してください。

nvm install 22
nvm use 22

Twilio CLI と Flex Plugins CLI のインストール

npm i -g twilio-cli
twilio plugins:install @twilio-labs/plugin-flex

成功すれば次のコマンドが通ります。

twilio --version
twilio flex:plugins --help

認証プロファイルの作成

Twilio CLI は名前付きのプロファイルとして認証情報を保持できます。macOS の場合は OS の Keychain に格納されます。

twilio profiles:create AC<your-account-sid> --auth-token <your-auth-token> -p flex-playground
twilio profiles:use flex-playground

AC<your-account-sid><your-auth-token> は、Twilio Console の Account タブから取得できます。なお、シークレットの管理方針は読者の所属環境のポリシーに従ってください。

Plugin 雛形の作成と起動

Flex Plugins CLI には、雛形を生成するサブコマンドが用意されています。

twilio flex:plugins:create plugin-flex-playground --install

--install オプションで npm 依存も同時に取得します。生成されるディレクトリの主要ファイルは次の通りです。

  • src/index.js
    Plugin のエントリポイント。Plugin クラスを loadPlugin に渡すだけ
  • src/FlexPlaygroundPlugin.js
    Plugin クラスの本体。init メソッドの中でカスタマイズを書く
  • src/components/CustomTaskList/CustomTaskList.jsx
    雛形の動作確認用に最初から入っているサンプル React コンポーネント
  • public/appConfig.js
    Plugin Service の有効化やログレベルなど、Flex UI 起動時の設定

CustomTaskList は雛形に最初から入っている、This is a dismissible demo component. という文言を出すだけのサンプル React コンポーネントです。本記事のカスタマイズの本筋ではないので、そのまま残しておきます。

dev サーバーを起動します。

cd plugin-flex-playground
twilio flex:plugins:start

起動するとブラウザが立ち上がり、http://localhost:3000/ で Flex のログイン画面が見えます。

login page

Log in with Twilio から SSO ログインすると、Flex UI が描画されます。

console top

見た目をカスタマイズする

ここから本題です。src/FlexPlaygroundPlugin.jsinit メソッドにカスタマイズコードを追加していきます。最終的なコード全体を先に示し、各サブ節で該当部分を引用しながら説明します。

import React from 'react';
import { FlexPlugin } from '@twilio/flex-plugin';

import CustomTaskList from './components/CustomTaskList/CustomTaskList';

const PLUGIN_NAME = 'FlexPlaygroundPlugin';

export default class FlexPlaygroundPlugin extends FlexPlugin {
  constructor() {
    super(PLUGIN_NAME);
  }

  async init(flex, manager) {
    this.applyThemeOverrides(manager);
    this.applyStringOverrides(manager);
    this.addHeaderBrand(flex);
    this.attachIncomingTaskNotification(manager);

    flex.AgentDesktopView.Panel1.Content.add(
      <CustomTaskList key="FlexPlaygroundPlugin-component" />,
      { sortOrder: -1 },
    );
  }

  applyThemeOverrides(manager) {
    manager.updateConfig({
      theme: {
        isLight: true,
        tokens: {
          backgroundColors: {
            colorBackgroundBody: '#eef4fa',
            colorBackgroundPrimary: '#0a4d8c',
            colorBackgroundPrimaryStrong: '#083e72',
          },
          textColors: {
            colorTextLink: '#0a4d8c',
          },
        },
      },
    });
  }

  applyStringOverrides(manager) {
    Object.assign(manager.strings, {
      NoTasks: 'タスク待ちです',
      NoTasksHintNotAvailable: 'アクティビティを切り替えるとタスクが届きます。',
      NoCRMConfigured: 'CRM 未設定',
      NoCRMHint: 'ドキュメントを参考に設定してください。',
    });
  }

  addHeaderBrand(flex) {
    flex.MainHeader.Content.add(
      <span
        key="flex-playground-brand"
        style={{ fontWeight: 700, marginLeft: 12 }}
      >
        Flex Playground
      </span>,
      { sortOrder: -100, align: 'start' },
    );
  }

  attachIncomingTaskNotification(manager) {
    if (!manager.workerClient || typeof manager.workerClient.on !== 'function') {
      return;
    }
    manager.workerClient.on('reservationCreated', () => {
      this.playBeep();
      this.tryVibrate();
    });
  }

  playBeep() {
    try {
      const AudioCtx = window.AudioContext || window.webkitAudioContext;
      if (!AudioCtx) {
        return;
      }
      const ctx = new AudioCtx();
      const osc = ctx.createOscillator();
      const gain = ctx.createGain();
      osc.connect(gain);
      gain.connect(ctx.destination);
      osc.type = 'sine';
      osc.frequency.value = 880;
      gain.gain.value = 0.1;
      osc.start();
      window.setTimeout(() => {
        osc.stop();
        ctx.close();
      }, 300);
    } catch (e) {
      console.warn('FlexPlayground: audio playback failed', e);
    }
  }

  tryVibrate() {
    if (typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function') {
      navigator.vibrate([200, 100, 200]);
    }
  }
}

init から呼び出す形に分解しているのは、各カスタマイズの責務を見やすくするためです。以降は項目ごとに該当部分を引用して説明します。なお Plugin の init はページ読み込み時に 1 回だけ実行されます。Plugin コードを変更したらブラウザのリロードが必要です。

配色テーマを上書きする

applyThemeOverrides(manager) {
  manager.updateConfig({
    theme: {
      isLight: true,
      tokens: {
        backgroundColors: {
          colorBackgroundBody: '#eef4fa',
          colorBackgroundPrimary: '#0a4d8c',
          colorBackgroundPrimaryStrong: '#083e72',
        },
        textColors: {
          colorTextLink: '#0a4d8c',
        },
      },
    },
  });
}

Flex UI v2 のテーマは Twilio Paste の design tokens で構成されています。design tokens とは、色、フォントサイズ、余白などを名前で参照できるようにした値の辞書です。Paste は Twilio が提供する独自のデザインシステムで、Flex UI もその上に組まれています。

manager.updateConfig({ theme: ... }) で渡した tokens は、デフォルト値の上に部分的に上書きされます。背景色、文字色、枠線色など系統別に分かれていて、上記では backgroundColorstextColors を変更しました。token の名前は Paste 側のドキュメントに揃っています。

blue background

ヘッダーにブランドテキストを追加する

addHeaderBrand(flex) {
  flex.MainHeader.Content.add(
    <span
      key="flex-playground-brand"
      style={{ fontWeight: 700, marginLeft: 12 }}
    >
      Flex Playground
    </span>,
    { sortOrder: -100, align: 'start' },
  );
}

flex.MainHeader.Content.add() は、Programmable Components という Flex UI の拡張機構を使うものです。Programmable Components は Flex UI 上にあらかじめ用意されている拡張可能なコンポーネントの総称で、各コンポーネントは Content.add(), .replace(), .remove() などのメソッドを持ちます。

sortOrder は数値が小さいほど先頭寄りに配置されます。デフォルトは 0 で、上記で -100 を指定したのは既存のヘッダー要素より左側に出したかったためです。alignstart または end を取り、ヘッダーの左寄せ / 右寄せを切替えます。

edit header

文言を日本語化する

applyStringOverrides(manager) {
  Object.assign(manager.strings, {
    NoTasks: 'タスク待ちです',
    NoTasksHintNotAvailable: 'アクティビティを切り替えるとタスクが届きます。',
    NoCRMConfigured: 'CRM 未設定',
    NoCRMHint: 'ドキュメントを参考に設定してください。',
  });
}

Flex UI 上の文言は、manager.strings という辞書オブジェクトで管理されています。キー名を指定して値を上書きすると、UI 上の表示が差し替わります。

実際にこの辞書には何個のキーがあるのでしょうか。Manager 初期化後に Object.keys(manager.strings).length を取得すると 1,611 でした (検証時の Flex UI v2.16.0) 。1,611 個のキーから所望のキー名を見つけるには、ブラウザの開発者ツールで manager.strings を直接覗くか、Flex UI のバンドル (@twilio/flex-uibundle/twilio-flex.prod.js) をテキスト検索する方法があります。検索するときは画面に出ている英文の前後の文字列を当たると、対応するキー名が引っかかります。

上書きできない文言

文言の中には、manager.strings 経由で上書きできないものもあります。代表的なのは Activity 名 (Available / Unavailable / Offline / Break) です。これらは TaskRouter Workspace の Activity リソースとして登録されている名前そのもので、Flex UI の文字列辞書とは別物です。これを変更したい場合は Twilio Console や API で TaskRouter Workspace の Activity を編集する必要があります。

通話状態を表す文言 (CALL ENDED, Connected など) も manager.strings で上書きできるキー群が別に存在しますが、本記事では深掘りしません。興味があれば手元の Flex でキーを探してみてください。

Japanese messages

着信時に音を鳴らしてみる

ここまでは見た目の話でしたが、せっかくなので動きのあるカスタマイズも試してみます。React 開発者の遊び心として、着信時にビープ音を鳴らしてみます。

イベントへのフック

attachIncomingTaskNotification(manager) {
  if (!manager.workerClient || typeof manager.workerClient.on !== 'function') {
    return;
  }
  manager.workerClient.on('reservationCreated', () => {
    this.playBeep();
    this.tryVibrate();
  });
}

着信があると、TaskRouter からエージェントに対して Reservation (予約) が生成されます。manager.workerClient.on('reservationCreated', ...) で Reservation 生成イベントにフックできます。

Web Audio API でビープ音を生成する

playBeep() {
  try {
    const AudioCtx = window.AudioContext || window.webkitAudioContext;
    if (!AudioCtx) {
      return;
    }
    const ctx = new AudioCtx();
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.connect(gain);
    gain.connect(ctx.destination);
    osc.type = 'sine';
    osc.frequency.value = 880;
    gain.gain.value = 0.1;
    osc.start();
    window.setTimeout(() => {
      osc.stop();
      ctx.close();
    }, 300);
  } catch (e) {
    console.warn('FlexPlayground: audio playback failed', e);
  }
}

Web Audio API でサイン波を 880Hz で 300 ミリ秒だけ鳴らす実装です。音源ファイルを別途用意せずに済むので簡単です。

注意点として、ブラウザには自動再生制限があります。ユーザーが画面と一度も対話していない状態では音が出ない場合があります。本検証ではエージェントが Activity を Available に切り替える操作を経たあとに着信があったので、問題なく鳴りました。読者が試す場合も、ログイン後に何かボタンをクリックした状態で着信を受けると確実です。

振動 (Web Vibration API)

tryVibrate() {
  if (typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function') {
    navigator.vibrate([200, 100, 200]);
  }
}

ついでに navigator.vibrate(...) も呼んでいます。Web Vibration API は Android Chrome では 200ms 振動 → 100ms 休止 → 200ms 振動のパターンを実行します。デスクトップブラウザや iOS Safari では呼び出しても何も起きません。本記事では Android 実機での検証はしていませんが、Flex を Android 端末から開く運用がある場合の遊び心として残しておきます。

通話検証

ここまでの実装をブラウザリロードで反映したあと、実際に着信させてみます。

  1. Flex UI の左下にあるアクティビティを Available に切替える
  2. 個人の携帯電話から、Twilio で確保している電話番号に発信する
  3. Studio Flow の Voice IVR が応答したあと、Flex UI に着信タスクカードが現れる
  4. 緑色のチェックボタンで応答 → 通話 → 通話終了後に表示される Complete ボタンで完了

get call

着信中のタスクカード

calling

通話中の画面

call end

通話終了後

着信のタイミングで playBeep のサイン波が鳴ったのを確認できました。

本番反映までのライフサイクル (概念のみ)

ここまでローカルの dev サーバーで動かしてきました。実際にコールセンターを運用するときには、ローカルで動いた Plugin を本番の Flex に乗せる工程が要ります。本記事ではコマンドの実演はしませんが、開発者・運用者が次に取り組むことになる流れを、概念ベースで紹介しておきます。

3 段階のコマンドで進みます。

  • twilio flex:plugins:build ・・・ webpack で本番向けの最適化バンドルを生成します。後段の deploy が内部で呼ぶので明示実行は省略できます
  • twilio flex:plugins:deploy ・・・ ビルドしたバンドルを Twilio の Plugins Service にアップロードし、Plugin Version (例 : 0.0.1) として登録します。Version は一度作るとイミュータブルです
  • twilio flex:plugins:release ・・・ 複数の Plugin Version の組み合わせを指す Configuration を新規作成し、それを Release として現行に切り替えます。これで本番の flex.twilio.com を開いたエージェントのブラウザに、自分の Plugin が読み込まれます

概念の階層は次の通りです。

  • Plugin
    自分が書く JavaScript モジュール。Plugin 名で一意に識別される
  • Plugin Version
    特定バージョンの成果物。一度デプロイされたら変更不可 (イミュータブル)
  • Configuration
    有効化したい Plugin Version の組み合わせを名前付きで束ねたもの。これも一度作ったら変更不可
  • Release
    現在アクティブな Configuration を指す可変ポインタ。これを書き換えると本番に反映される

切り戻しは、過去の Configuration を指す新しい Release を作るだけで完了します。Configuration は消えないので、いつでも過去の任意の組み合わせに戻せます。

環境の分離

Twilio Flex には dev / staging / prod の概念が組み込まれていません。一般には次のいずれかで対応します。

  • アカウントを開発用と本番用で分け、Twilio CLI のプロファイルを切り替えてデプロイする
  • Plugin の挙動を Worker の attributes やフィーチャーフラグで切り分け、Plugin Version の数を増やしすぎないように運用する

ローカル dev サーバーの起動オプションに --include-remote を付けると、すでに本番 Release されている Plugin をベースにしてローカル Plugin だけを差し込めます。本番に近い状態でのプレビューもできるので、本番反映前の確認に有用です。

まとめ

本記事では、Twilio Flex の React UI を Plugin でカスタマイズし、ローカル dev サーバー上で動作確認するまでの流れを試しました。配色・ヘッダーブランド・文言・着信音までを実装し、Activity 名のような TaskRouter Workspace 由来のラベルは文字列辞書では変更できないことも確認しました。

本番反映には Plugins Service を介した build / deploy / release のライフサイクルがあり、本記事では概念のみ紹介しました。本記事が、Twilio Flex のカスタマイズに取り組む皆様の参考になれば幸いです。

この記事をシェアする

関連記事