Customizing the Look and Feel of Twilio Flex with a React Plugin

Customizing the Look and Feel of Twilio Flex with a React Plugin

Customize Twilio Flex's React-based contact center UI with a plugin on a local dev server: colors, header branding, text and ringtones. Also covers the lifecycle to production at a conceptual level.
2026.04.30

This page has been translated by machine translation. View original

Introduction

Twilio Flex is a contact center platform provided by Twilio. The agent-facing UI is built with React, so React engineers can insert components they have written themselves to customize the look and behavior.

ui customize

In this article, aimed at React developers touching Flex for the first time, we will try the following four things on a local development server.

  • Rewrite the color theme
  • Insert your own brand text into the header
  • Replace English text in the UI with Japanese
  • Play a beep with the Web Audio API when a call comes in

We will stop at the local dev server, but near the end of the article I will introduce the lifecycle up to production deployment at a conceptual level. The structure is designed so you can picture what the flow from development to production looks like when actually operating a call center.

For activating a Twilio account and the initial setup of a Flex project, please refer to the following article.

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

This article assumes that state and starts from the setup for Plugin development.

What is Twilio Flex?

Twilio Flex is a cloud-based contact center platform that consolidates channels such as phone, SMS, web chat, and email into a single agent-facing UI. Its distinguishing feature is that the screens agents use to handle customers are built with React. You can extend the look and behavior with JavaScript modules (Plugins) that you write yourself.

Do you need your own server?

Not for normal use. The Flex UI itself, Plugin delivery, TaskRouter (task routing), and backend services such as Voice / SMS / Studio are all handled within SaaS hosted by Twilio. Plugins are also uploaded to and delivered from the Twilio Plugins Service.

The local dev server that appears during development is separate from any server used in production operation. What twilio flex:plugins:start launches is really webpack-dev-server, which is merely used to build and serve the JavaScript bundle of your own Plugin locally. The browser opens http://localhost:3000/, but the Flex UI itself is loaded from Twilio's CDN (assets.flex.twilio.com). The accurate understanding is not that Flex runs entirely on localhost, but that only your Plugin is injected locally into the Flex UI hosted by Twilio.

Operating additional servers on the user side becomes necessary only when you want to integrate your own backend (for example, queries to your company's CRM or calls to your own APIs).

Overall architecture

When a customer sends a call or message, Voice / SMS / Conversations on the Twilio cloud side receive it. Studio Flow (a no-code flow definition tool) handles the response logic, and TaskRouter routes the task to an agent. The agent receives the task in the Flex UI. The overall flow is that custom Plugins are loaded into the Flex UI via the Plugins Service.

Intended audience

  • React engineers touching Twilio Flex for the first time
  • Developers and operators who want to customize the look of contact center screens with Plugins
  • Those who want to grasp, at a conceptual level, the flow from local development to production deployment

References

Setting up the development environment

From here we get hands-on. The environment for this verification is as follows.

  • macOS, WebStorm
  • Node.js v22 series (v22 is used because of the constraints described below)

Node.js version

The latest Twilio CLI (version 6) and Flex Plugins CLI (version 7) each have constraints on the Node.js version. In particular, @twilio/flex-plugin-scripts v7.1.2 requires ^16 || ^18 || ^20 || ^22 in its engine field, and in an environment with Node v24 or later, twilio plugins:install @twilio-labs/plugin-flex after npm i -g twilio-cli fails with an engine "node" is incompatible error.

My environment had Node v24 as the default, but I switched to v22 via nvm to do the work. If you are using the v24 series, please use v22 alongside it with nvm, fnm, or similar.

nvm install 22
nvm use 22

Installing Twilio CLI and Flex Plugins CLI

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

If it succeeds, the following commands will work.

twilio --version
twilio flex:plugins --help

Creating an authentication profile

Twilio CLI can keep credentials as named profiles. On macOS they are stored in the 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> and <your-auth-token> can be obtained from the Account tab in the Twilio Console. Please follow the policies of your own organization for how to manage secrets.

Creating and launching the Plugin scaffold

Flex Plugins CLI provides a subcommand that generates a scaffold.

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

The --install option fetches npm dependencies at the same time. The main files in the generated directory are as follows.

  • src/index.js
    The Plugin entry point. It just passes the Plugin class to loadPlugin
  • src/FlexPlaygroundPlugin.js
    The body of the Plugin class. Write customizations inside the init method
  • src/components/CustomTaskList/CustomTaskList.jsx
    A sample React component included from the start to verify that the scaffold works
  • public/appConfig.js
    Settings applied when the Flex UI starts, such as enabling Plugin Service and the log level

CustomTaskList is a sample React component included in the scaffold from the start that simply displays the text This is a dismissible demo component. It is not the main thread of the customizations in this article, so I leave it as is.

Start the dev server.

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

When it starts, the browser opens and you can see the Flex login screen at http://localhost:3000/.

login page

When you log in via SSO with Log in with Twilio, the Flex UI is rendered.

console top

Customizing the look

Now for the main topic. We will add customization code to the init method in src/FlexPlaygroundPlugin.js. I show the final code in full first, then explain each part by quoting the relevant portions in each subsection.

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

I split the code into methods called from init to make the responsibility of each customization easy to see. From here on, I will quote and explain the relevant portion for each item. Note that a Plugin's init runs only once at page load. After changing the Plugin code, you need to reload the browser.

Overriding the color theme

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

The Flex UI v2 theme is built from Twilio Paste design tokens. Design tokens are a dictionary of values that lets you refer to colors, font sizes, spacing, and so on by name. Paste is Twilio's own design system, and the Flex UI is built on top of it.

The tokens passed via manager.updateConfig({ theme: ... }) partially override the default values. They are divided by category such as background colors, text colors, and border colors, and above I changed backgroundColors and textColors. Token names match the documentation on the Paste side.

blue background

Adding brand text to the header

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() uses Programmable Components, an extension mechanism of the Flex UI. Programmable Components is a general term for the extensible components prepared in advance on the Flex UI, and each component has methods such as Content.add(), .replace(), and .remove().

The smaller the sortOrder value, the closer to the front the element is placed. The default is 0, and I specified -100 above because I wanted it to appear to the left of the existing header elements. align takes start or end and switches between left-aligned and right-aligned in the header.

edit header

Translating text into Japanese

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

Text in the Flex UI is managed by a dictionary object called manager.strings. If you specify a key name and overwrite its value, the display in the UI is replaced.

How many keys does this dictionary actually have? Getting Object.keys(manager.strings).length after Manager initialization gave 1,611 (Flex UI v2.16.0 at the time of verification). To find the key name you want among 1,611 keys, you can either inspect manager.strings directly in the browser's developer tools, or text-search the Flex UI bundle (bundle/twilio-flex.prod.js in @twilio/flex-ui). When searching, look for the strings before and after the English text shown on screen, and the corresponding key name will turn up.

Text that cannot be overridden

Some text cannot be overridden via manager.strings. A typical example is Activity names (Available / Unavailable / Offline / Break). These are the names registered as Activity resources in the TaskRouter Workspace, and are separate from the Flex UI string dictionary. To change them, you need to edit the Activities of the TaskRouter Workspace via the Twilio Console or API.

There is also a separate group of keys in manager.strings that can override text representing call states (CALL ENDED, Connected, etc.), but this article does not go deeper into them. If you are interested, try looking for the keys in your own Flex.

Japanese messages

Playing a sound on incoming calls

So far it has been about appearance, but since we are here, let's try a customization with some motion too. As a bit of fun for a React developer, let's play a beep when a call comes in.

Hooking into the event

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

When a call comes in, TaskRouter creates a Reservation for the agent. With manager.workerClient.on('reservationCreated', ...) you can hook into the Reservation creation event.

Generating a beep with the 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);
  }
}

This implementation plays a sine wave at 880Hz for just 300 milliseconds with the Web Audio API. It is easy because you don't need to prepare a separate audio file.

Note that browsers have autoplay restrictions. Sound may not play if the user has never interacted with the page. In this verification, the call came in after the agent had switched the Activity to Available, so it played without problems. If you try it yourself, receiving the call after clicking some button following login is the safest.

Vibration (Web Vibration API)

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

I also call navigator.vibrate(...) while at it. On Android Chrome, the Web Vibration API runs a pattern of 200ms vibration → 100ms pause → 200ms vibration. On desktop browsers and iOS Safari, calling it does nothing. I did not verify it on an actual Android device in this article, but I leave it in as a bit of fun in case you operate Flex by opening it from Android devices.

Call verification

After reflecting the implementation so far by reloading the browser, let's actually place a call.

  1. Switch the activity at the bottom left of the Flex UI to Available
  2. Call the phone number secured in Twilio from a personal mobile phone
  3. After the Studio Flow Voice IVR responds, an incoming task card appears in the Flex UI
  4. Answer with the green check button → talk → after the call ends, complete with the Complete button that appears

get call

Task card during an incoming call

calling

Screen during a call

call end

After the call ends

I confirmed that the sine wave from playBeep sounded at the moment of the incoming call.

Lifecycle up to production deployment (concept only)

So far we have run things on the local dev server. When actually operating a call center, you need a process to put the Plugin that worked locally onto the production Flex. This article does not demonstrate the commands, but I introduce at a conceptual level the flow that developers and operators will tackle next.

It proceeds with three stages of commands.

  • twilio flex:plugins:build ... Generates an optimized bundle for production with webpack. The later deploy calls it internally, so explicit execution can be omitted
  • twilio flex:plugins:deploy ... Uploads the built bundle to Twilio's Plugins Service and registers it as a Plugin Version (e.g., 0.0.1). A Version is immutable once created
  • twilio flex:plugins:release ... Creates a new Configuration that points to a combination of multiple Plugin Versions and switches it to be current as a Release. With this, your Plugin is loaded in the browser of agents who open the production flex.twilio.com

The hierarchy of concepts is as follows.

  • Plugin
    The JavaScript module you write. Uniquely identified by the Plugin name
  • Plugin Version
    The artifact of a specific version. Cannot be changed once deployed (immutable)
  • Configuration
    A named bundle of the combination of Plugin Versions you want to enable. This also cannot be changed once created
  • Release
    A mutable pointer to the currently active Configuration. Rewriting it reflects the change in production

Rollback is completed simply by creating a new Release that points to a past Configuration. Configurations are not deleted, so you can always return to any past combination.

Separating environments

Twilio Flex has no built-in concept of dev / staging / prod. Generally, you handle it with one of the following.

  • Separate accounts for development and production, and deploy by switching Twilio CLI profiles
  • Separate Plugin behavior using Worker attributes or feature flags, and operate so as not to increase the number of Plugin Versions too much

If you add the --include-remote option when starting the local dev server, you can inject only the local Plugin on top of the Plugins already released in production. You can also preview in a state close to production, which is useful for checking before deploying to production.

Summary

In this article, we tried the flow of customizing the React UI of Twilio Flex with a Plugin and verifying it on a local dev server. We implemented the color scheme, header brand, text, and ringtone, and also confirmed that labels originating from the TaskRouter Workspace, such as Activity names, cannot be changed with the string dictionary.

For production deployment there is a build / deploy / release lifecycle via the Plugins Service, which this article introduced only conceptually. I hope this article is helpful to everyone working on customizing Twilio Flex.


Twilioの導入支援はクラスメソッドにお任せください!

クラスメソッドでは、Twilioの導入から運用までしっかりサポートいたします。
コミュニケーションツールの導入や最適化をお考えの方は、ぜひお気軽にご相談ください。

Twilioの詳細を見る

Share this article