---
title: "How to create a widget for streams with remote control"
description: "In this article, we’ll go step by step to create a stream widget with remote control. You can follow along and end up with a working widget."
lang: en
canonical_url: "https://docs.wotstat.info/en/articles/how-to-create-remote-control-widget/"
markdown_url: "https://docs.wotstat.info/en/articles/how-to-create-remote-control-widget/index.md"
source_url: "https://github.com/wotstat/mods-development-docs/blob/main/docs/en/articles/how-to-create-remote-control-widget/index.md"
---

# How to create a widget for streams with remote control

In this article, we’ll go **step by step** to create a stream widget with remote control. You can follow along and end up with a working widget.

Video: [open media](https://docs.wotstat.info/en/articles/how-to-create-remote-control-widget/assets/hero-widget.mp4)

## Preparation

All widgets are regular websites that run in the built-in browser of `OBS Studio`. Therefore, to create a widget you’ll need a standard web-development toolset.

To keep things simple, we won’t use any web frameworks and will implement everything with plain `HTML`, `CSS`, and `JavaScript`. However, we’ll still use `Vite` (a tool that simplifies development and bundling for publication).

### BunJS
Install `BunJS` — a modern package manager and JavaScript runtime. You can find installation instructions on the [official website](https://bun.sh/).

**Windows**

```powershell
powershell -c "irm bun.sh/install.ps1 | iex"
```

**Linux/MacOS**

```bash
curl -fsSL https://bun.sh/install | bash
```

### Visual Studio Code
You’ll also need `VSCode` — the code editor we’ll use for development. Download it from the [official website](https://code.visualstudio.com/).

### Project creation
Create a new folder for your project and open it in `VSCode`. Then open the terminal in `VSCode` (`Terminal` → `New Terminal`) and run:

```bash
bun create vite . --template vanilla
```

Press `Enter` a few times to accept the defaults.

**Details — Terminal output after running the command**

![vite-create-terminal-output](https://docs.wotstat.info/en/articles/how-to-create-remote-control-widget/assets/vite-create-terminal-output.png)

Your project will be set up, dependencies installed, and a local development server started (usually at `http://localhost:5174/`).

Open that address in your browser—you should see the `Hello Vite!` starter page.

Clean up the starter page: in `VSCode`, delete the `public` folder and the files `src/counter.js` and `src/javascript.svg`.

Edit the `src/main.js` file, removing everything except the stylesheet import:

**src/main.js**

```javascript
import './style.css'
```

Also completely clear the `src/style.css` file, leaving it empty.

Done! Thanks to `Vite`, every change in your files will be automatically reflected in the browser. You should now see a blank white page.

## Creating the widget
Development will consist of two stages:
- Create the widget’s visual design
- Add remote-control functionality

### Visual design of the widget
The widget’s design is completely up to you and depends on your task; the process is no different from building a regular website. In this article, we’ll create a simple LBZ (Personal Missions) counter. It consists of three blocks, each with a progress bar and the number of completed LBZ.

The only difference from a regular website is the widget’s dimensions. We’ll make it automatically fit the screen width so the streamer can use it at any resolution. To do this, we’ll set the font size to `1%` of the viewport width and specify all sizes in `em`.

**src/style.css**

```css
:root {
  /* Transparent background */
  color-scheme: dark;
  background: transparent;
}

body {
  /* Set font size based on window width */
  font-size: 1vw;

  /* Remove margin and hide scrollbars */
  margin: 0;
  overflow: hidden;
}
```

Now create the general structure of the widget in `index.html`:
**index.html**

```html
...
<div id="app">
  <div class="item item-1">
    <header>
      <h2>VI - VII уровень</h2>
      <p><span class="current">18</span> / 25</p>
      <img src="./src/assets/complete.png">
    </header>
    <div class="progress-bar">
      <div class="progress-bar-background"></div>
      <div class="progress-bar-pattern"></div>
      <div class="progress-bar-blink"></div>
    </div>
  </div>
</div>
...
```

Duplicate the `div.item` block twice, change the `h2` heading text, and add the `completed` class to the last `div.item`.

We’ll need some assets from the game, which you can find in the `gui.pkg` package or in the [Kurzdor/wot.assets](https://github.com/Kurzdor/wot.assets/tree/Lesta) repository.

- The font can be found at [`gui/gameface/fonts/Warhelios-Regular.ttf`](https://github.com/Kurzdor/wot.assets/tree/Lesta/gui/gameface/fonts/Warhelios-Regular.ttf)
- Progress-bar parts at [`/gui/maps/icons/components/progress_bar`](https://github.com/Kurzdor/wot.assets/tree/Lesta/gui/maps/icons/components/progress_bar)
- The checkmark for a completed stage at [`/gui/maps/icons/personalMissions3/QuestsView/complete.png`](https://github.com/Kurzdor/wot.assets/tree/Lesta/gui/maps/icons/personalMissions3/QuestsView/complete.png)

Copy the required assets into the `src/assets` folder of your project.

Adjust the styles in `style.css` to achieve the following result:

**Details — Full `style.css` source**

**src/style.css**

````css
:root {
  /* Transparent background */
  color-scheme: dark;
  background: transparent;
}

@font-face {
  font-family: Warhelios;
  src: url(./assets/Warhelios-Regular.ttf) format("truetype");
}

body {
  /* Set font size based on window width */
  font-size: 1vw;

  /* Remove margin and hide scrollbars */
  margin: 0;
  overflow: hidden;

  font-family: Warhelios, sans-serif;
}

#app {
  display: flex;
  gap: 1em;
  padding: 1em;
}

.item {
  border-radius: 1.3em;
  padding: 1.2em;
  border: 0.25em solid rgba(255, 173, 65, 0.4);
  background:
    linear-gradient(0deg, rgba(195, 90, 4, 0.1), rgba(255, 255, 255, 0) 50%),
    radial-gradient(farthest-corner at 50% 200%, rgba(251, 166, 20, 0.4), rgba(218, 139, 1, 0.3) 90%);

  display: flex;
  flex-direction: column;
  flex: 1;

  max-width: 30%;
  box-shadow: 0 0 1em rgba(0, 0, 0, 0.2);
}

.completed {
  border: 0.25em solid rgba(106, 255, 65, 0.3);
  background:
    linear-gradient(0deg, rgba(80, 255, 27, 0.3), rgba(26, 255, 0, 0) 50%),
    radial-gradient(farthest-corner at 50% 200%, rgba(4, 102, 5, 0.4), rgba(13, 114, 0, 0.3) 90%);
}

header {
  font-size: 1em;
  display: flex;
}

header h2 {
  flex: 1;
}

header h2,
header p {
  font-size: 2.7em;
  margin: 0;
  font-weight: normal;
}

.current {
  color: #ffd28f;
}

img {
  width: 9em;
  height: 9em;
  margin: -3em;
  display: none;
}

/* Progress bar */
.progress-bar {
  height: 0.3em;
  width: 100%;
  margin-top: 1em;
  position: relative;
}

/* Background part of the progress bar (gray rectangles) */
.progress-bar-background {
  background-image: url(./assets/pattern_grey.png);
  background-repeat: repeat;
  background-position: 0 50%;
  background-size: 0.4em 1em;

  position: absolute;
  width: 100%;
  height: 100%;
  box-shadow: 0 0 2em rgba(0, 0, 0, 0.5);
}

/* Current progress highlight (glow) */
.progress-bar-blink {
  height: 5em;
  width: 5em;

  background-image: url(./assets//glow_small.png);
  background-size: contain;
  mix-blend-mode: lighten;
  position: absolute;
  transform: translate(-49%, -50%);
  top: 50%;
  left: var(--progress, 60%);
  transition: left 0.2s ease;
}

/* Filled part of the progress bar */
.progress-bar-pattern {
  background-image: url(./assets/pattern_orange.png);
  background-repeat: repeat;
  background-position: 0 50%;
  background-size: 0.4em 1em;

  position: absolute;
  height: 100%;
  width: var(--progress, 60%);
  transition: width 0.2s ease;
}

/* Gradient highlight for the progress bar */
.progress-bar-pattern::after {
  content: "";
  position: absolute;
  right: 0;
  top: 0;
  width: 100%;
  height: 100%;

  background-image: linear-gradient(90deg, rgba(0, 0, 0, 0.6), rgba(255, 206, 122, 0.5));
  mix-blend-mode: overlay;
}

/* Overrides for completed stages */
.completed .progress-bar-pattern {
  background-image: url(./assets/pattern_green.png);
  width: 100%;
}

.completed .progress-bar-pattern::after {
  background-image: linear-gradient(90deg, rgba(0, 0, 0, 0.6), rgba(187, 255, 78, 0.519), rgba(0, 0, 0, 0.6));
}

.completed .progress-bar-blink,
.completed header p {
  display: none;
}

.completed img {
  display: block;
}
````

**Details — Full `index.html` source**

**src/index.html**

````html
<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>article-pm-widget</title>
</head>

<body>
  <div id="app">
    <div class="item item-1">
      <header>
        <h2>VI - VII tire</h2>
        <p><span class="current">18</span> / 25</p>
        <img src="./src/assets/complete.png">
      </header>
      <div class="progress-bar">
        <div class="progress-bar-background"></div>
        <div class="progress-bar-pattern"></div>
        <div class="progress-bar-blink"></div>
      </div>
    </div>

    <div class="item item-2">
      <header>
        <h2>VII - IX tire</h2>
        <p><span class="current">18</span> / 25</p>
        <img src="./src/assets/complete.png">
      </header>
      <div class="progress-bar">
        <div class="progress-bar-background"></div>
        <div class="progress-bar-pattern"></div>
        <div class="progress-bar-blink"></div>
      </div>
    </div>

    <div class="item item-3 completed">
      <header>
        <h2>X - XI tire</h2>
        <p><span class="current">18</span> / 25</p>
        <img src="./src/assets/complete.png">
      </header>
      <div class="progress-bar">
        <div class="progress-bar-background"></div>
        <div class="progress-bar-pattern"></div>
        <div class="progress-bar-blink"></div>
      </div>
    </div>

    <script type="module" src="/src/main.js"></script>
</body>

</html>
````

As a result, you’ll get the following widget:
![widget-prepare-result.png](https://docs.wotstat.info/en/articles/how-to-create-remote-control-widget/assets/widget-prepare-result.png)

### Remote-control functionality
Now let’s add remote control. To do this, install the [`wotstat-widgets-sdk`](https://www.npmjs.com/package/wotstat-widgets-sdk) library and declare the widget’s remote parameters.

Run this command in the terminal:

**Windows**

```powershell
bun add wotstat-widgets-sdk
```

**Linux/MacOS**

```bash
bun add wotstat-widgets-sdk
```

In the `src/main.js` file, import the library and declare the widget parameters:

**src/main.js**

```javascript
import './style.css'
import { WidgetsRemote } from 'wotstat-widgets-sdk';

const remote = new WidgetsRemote();

remote.defineState('VI - VII уровень', 0, { elementHelper: '.item-1' });
remote.defineState('VII - IX уровень', 0, { elementHelper: '.item-2' });
remote.defineState('X - XI уровень', 0, { elementHelper: '.item-3' });
```

Now you can open your link in the widget control panel at [ru.widgets.wotstat.info/remote-control](https://ru.widgets.wotstat.info/remote-control) and make sure the widget parameters appear in the list; when you hover over a parameter, the corresponding widget block is outlined:
![widget-remote-control-params.png](https://docs.wotstat.info/en/articles/how-to-create-remote-control-widget/assets/widget-remote-control-params.png)

The outline works thanks to the `elementHelper` property, where we specified the CSS selector of the desired block.

Next, we need to handle parameter changes. Create an `updateState` function and subscribe to changes for each parameter using the `watch` method:
**src/main.js**

```javascript
...
function updateState(selector, value) {
  const element = document.querySelector(selector);

  // Set the counter value
  element.querySelector('.current').textContent = value;

  // Toggle the completed stage class
  if (value >= 25) element.classList.add('completed');
  else element.classList.remove('completed');

  // Add a CSS variable for the progress bar
  element.style.setProperty('--progress', `${100 * value / 25}%`);
}

remote.defineState('VI - VII уровень', 0, { elementHelper: '.item-1' })
  .watch((v) => updateState('.item-1', v));

remote.defineState('VII - IX уровень', 0, { elementHelper: '.item-2' })
  .watch((v) => updateState('.item-2', v));

remote.defineState('X - XI уровень', 0, { elementHelper: '.item-3' })
  .watch((v) => updateState('.item-3', v));
```

Done! Now, when the parameters change in the control panel, the widget updates automatically.

## Publishing the widget
The final step is to publish the widget so it’s available for use over the internet.

First, build the project. In the terminal, run:

**Windows**

```powershell
bun run build
```

**Linux/MacOS**

```bash
bun run build
```

As a result, a `dist` folder will be created containing the site ready for publication.

![terminal-build-output](https://docs.wotstat.info/en/articles/how-to-create-remote-control-widget/assets/terminal-build-output.png)

You can host this site on any static hosting, for example:
- [wasmer.io](https://wasmer.io/) — free after registration (you can sign in with Google)
- [Yandex Cloud](https://cloud.yandex.ru/) in S3 hosting mode — free, but registration is complicated
- [GitHub Pages](https://docs.github.com/en/pages/quickstart/) — free, but requires registration and creating a repository on GitHub

### Adding to OBS

After publishing, paste the widget link into the [remote control panel](https://ru.widgets.wotstat.info/remote-control), generate an access key, and add the widget to `OBS Studio` as a `Browser Source`, specifying the generated link.

> Don’t forget to choose `Blending Method` → `SRGB off`. You can study this in more detail in the [guide](https://docs.wotstat.info/guide/widgets/stream/index.md#widget-adding).

## Source code

All widget source code is available in the repository on [GitHub](https://github.com/SoprachevAK/mt-pm3-progress-widget). The result is published on GitHub Pages at: [soprachevak.github.io/mt-pm3-progress-widget](https://soprachevak.github.io/mt-pm3-progress-widget/) and in the control panel at [ru.widgets.wotstat.info/remote-control](https://ru.widgets.wotstat.info/remote-control?widget-url=aHR0cHM6Ly9zb3ByYWNoZXZhay5naXRodWIuaW8vbXQtcG0zLXByb2dyZXNzLXdpZGdldC8/cmVtb3RlLWtleT1oNXpvOVhFZ0NT)
