> For the complete documentation index, see [llms.txt](https://docs.kickbot.com/dashboard/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kickbot.com/dashboard/widgets/widget-builder-docs.md).

# Widget builder docs

Program custom widgets through the KickBot widget builder. Use HTML, CSS, and Javascript to build anything you can dream of. Easily integrate with stream events through the KickBot library.

### **Before you get started**

The KickBot widget builder is an advanced tool that requires some basic programming knowledge. If you are looking to use already made widgets, check out the widget library.

### Creating a new widget

Once you have navigated to the widget builder page, the first step is creating a new widget. You can either create a widget from a blank template, or you can start from an existing preset. Starting from an existing preset is a great way to get started and an easy starting point if you want to create something similar.

## Widget builder sidebar

The left sidebar of the widget builder controls a few different aspects of the widget.

### Basic settings

**Name** - The name of the widget

**Dimensions** - The pixel dimensions that the widget will be displayed as. It is recommended to use the default 800x600px resolution unless your widget benefits from a different size.

### User variables

Custom variables that the end user can adjust without needing to interact with the code. These can be accessed through `userVariables.variable_name.value` in your Javascript. As a widget developer there are several fields to configure when creating a variable:\
\
**Title** - The name of this field that is displayed to the end user.

**Variable name -** The name of the variable that you use in your code. No spaces or special characters allowed.

**Tooltip** - Extra information that should be shown to the user via a tooltip that appears when hovered over.

**Variable type -** The type of input for this variable. Options are text, number, color, dropdown, toggle, image/GIF, video, and audio. Do note that image, video, and audio values will return a URL to the media that is hosted in our storage.

**Default value** - The default value of this variable, this can be empty.

**Required** - Is the user required to enter a value.

### Send test events

Send test events to your widget to simulate stream events. These events have the same structure as actual events but may use fake data.

## Code editor

This code editor gives you full control over the HTML, CSS, and Javascript of the widget. The code runs in the widget page inside an OBS Browser Source. Treat all event fields and user variables as untrusted data. You can move and resize this editor, or change the layout with the buttons on the top right.

### Show chat messages safely

Before a widget receives a chat event, KickBot checks `data.content` and `data.sender.username` for executable syntax. It adds an invisible character after `<` when the text could start an HTML tag. It also breaks event handler names such as `onerror=` and adds an invisible character before the colon in executable URL schemes such as `javascript:`. Plain punctuation, emoticons such as `<3`, and normal URLs stay unchanged. The same checks apply to pinned messages, `globalValues.chatMessages`, and gift and raid message text.

The event fields are safe as HTML text, but they are not safe for every HTML context. Do not use chat text as a URL, CSS, or script. To show chat with Kick emotes, use `window.kickbotWidget.createChatMessageFragment`. It creates text nodes and emote images. It does not interpret message text as HTML. For other text fields, use `window.kickbotWidget.createTextNode`.

```javascript
document.addEventListener('widgetEvent', (event) => {
    if (event.detail.event_name !== 'chatMessageEvent') return;

    const chat_line = document.createElement('div');
    const username = document.createElement('strong');
    username.append(window.kickbotWidget.createTextNode(event.detail.data.sender.username + ': '));
    chat_line.append(username);
    chat_line.append(window.kickbotWidget.createChatMessageFragment(event.detail.data.content, 20));
    document.getElementById('chat-log').append(chat_line);
});
```

For viewer text outside these event fields, use `textContent`. If you must build an HTML string, apply `window.kickbotWidget.escapeHtmlText(value)` to text placed between HTML tags. Do not use this helper for URLs, CSS, or script code. Do not put raw event fields into `innerHTML`.

## Preview

The preview shows a realtime view of your widget. This automatically reloads based on code updates and is rendered in the resolution you defined for your widget, although it may be at a different scale. You also have the option to reload the preview, or pop out the widget into a new browser window.

TIP: To view the console of your widget, pop out the preview and inspect the new tab.

## Events

You can easily listen to events through our simplified event system. Through a single event listener you are able to receive all relevant events from a stream such as chat messages, sub, follows, viewer count updates, raids, and more.

#### Example event usage

```javascript
document.addEventListener('widgetEvent', handleWidgetEvent);

const eventHandlers = {
    followEvent: function(event) {
        updateFollowerGoal(event.data.followers_count);
    },
    unfollowEvent: function(event) {
        updateFollowerGoal(event.data.followers_count);
    },
    followerCountUpdatedEvent: function(event) {
        updateFollowerGoal(event.data.followersCount);
    }
};

function handleWidgetEvent(event) {
    const eventName = event.detail.event_name;
    if (eventHandlers[eventName]) {
        eventHandlers[eventName](event.detail);
    }
}

function updateFollowerGoal(followersCount) {
    const progressBar = document.getElementById('progress-bar');
    const currentFollowersElem = document.getElementById('current-followers');
      let goalFollowers = userVariables.follower_goal.value;

    currentFollowersElem.textContent = followersCount;
    const progressPercent = (followersCount / goalFollowers) * 100;
    progressBar.style.width = `${Math.min(progressPercent, 100)}%`;
}
```

### Event examples

`subEvent`

```javascript
{
    event_name: 'subEvent',
    source: 'kick',
    data: {
	months: 1,
	username: 'henryk89',
	chatroom_id: 3847194
    }
}
```

***

`subGiftEvent`

```javascript
{
    event_name: 'subGiftEvent',
    source: 'kick',
    data: {
	chatroom_id: 1,
	gifted_usernames: ['henryk89', 'KickBot', 'AdinRoss'],
	gifter_username: 'Eddie'
    }
}
```

***

`followEvent`

```javascript
{
    event_name: 'followEvent',
    source: 'kick',
    data: {
	id: 'd4e5f6a7-89b0-12c3-d456-78901e2f3a4b',
	channel: { id: 3984507, slug: 'henryk89' },
	user: { id: 14873539, slug: 'henryalt89', username: 'henryalt89' },
        followers_count: 36,
        created_at: '2024-05-13T22:19:07+00:00'
    }
}
```

***

`unfollowEvent`

```javascript
{
    event_name: 'unfollowEvent',
    source: 'kick',
    data: {
        id: 'f746861e-1114-43eb-b079-a1e119c8becd',
	channel: { id: 3984507, slug: 'henryk89' },
	user: { id: 14873539, slug: 'henryalt89', username: 'henryalt89' },
	followers_count: 35,
	created_at: '2024-05-13T22:26:45+00:00'
    }
}
```

***

`chatMessageEvent`

```javascript
{
    event_name: 'chatMessageEvent',
    source: 'kick',
    data: {
	id: '85c884b3-7377-478c-b6aa-22b7db752b93',
	chatroom_id: 3974112,
	content: 'test',
	type: 'message',
	created_at: '2024-05-13T22:29:52+00:00',
	sender: {
		id: 4063326,
		username: 'henryk89',
		slug: 'henryk89',
		identity: {
			color: '#72ACED',
			badges: [{ type: 'broadcaster', text: 'Broadcaster' }]
		}
	}
    }
}
```

***

`chatMessageDeletedEvent`

```javascript
{
    event_name: 'chatMessageDeletedEvent',
    source: 'kick',
    data: {
	id: '9cd3788a-ecd2-4051-a162-a7ea9be8ce7a',
	message: { id: '89d6be86-63db-4581-922e-14a6c4a9c2e6' }
    }
}
```

***

`chatMessagePinnedEvent`

```javascript
{
    event_name: 'chatMessagePinnedEvent',
    source: 'kick',
    data: {
	message: {
		id: '5d798a38-ad17-45e4-8169-bda2faa85b79',
		chatroom_id: 3974112,
		content: 'This is a pinned message!',
		type: 'message',
		created_at: '2024-05-13T22:30:41+00:00',
		sender: {
			id: 4063326,
			username: 'henryk89',
			slug: 'henryk89',
			identity: {
				color: '#72ACED',
				badges: [{ type: 'broadcaster', text: 'Broadcaster' }]
			}
		}
	}
    }
}
```

***

`chatMessageUnpinnedEvent`

```
{
    event_name: 'chatMessageUnpinnedEvent',
    source: 'kick',
    data: []
}
```

***

`chatClearedEvent`

```javascript
{
    event_name: 'chatClearedEvent',
    source: 'kick',
    data: {
	id: '47e3596f-0274-46e0-85b1-029fa3bfc5ea'
    }
}
```

***

`followerCountUpdatedEvent`

```javascript
{
    event_name: 'followerCountUpdatedEvent',
    source: 'kick',
    data: {
	followersCount: 252545,
	channel_id: 4599,
	created_at: 1715639984
    }
}
```

***

`viewerCountUpdatedEvent`

```javascript
{
    event_name: 'viewerCountUpdatedEvent',
    source: 'kick',
    data: {
	id: 28346454,
	viewer_count: 3204,
	channel_id: 5937485
    }
}
```

***

`raidEvent`

```javascript
{
    event_name: 'raidEvent',
    source: 'kick',
    data: {
        chatroom_id: 839485,
	optional_message: 'pog stream',
	number_viewers: 19384,
	host_username: 'xQc'
    }
}
```

***

`streamOnline`

```javascript
{
    event_name: 'streamOnline',
    source: 'kick',
    data: {
 	livestream: {
            id: 3984507,
	    channel_id: 3984507,
	    session_title: 'Test stream!',
	    created_at: '2024-05-13T22:30:41+00:00'
        }
    }
}
```

***

`streamOffline`

```javascript
{
    event_name: 'streamOnline',
    source: 'kick',
    data: {
	livestream: {
		id: 3984507,
		channel: {
			id: 3984507,
			is_banned: false
		}
	}
    }
}
```

## Global values

Global values provide you with information relevant to the streamer and stream. These values are automatically updated based on events, giving you an easily accessible global state for things like the current follower count. These values are initialized on page load, allowing you to populate your widget before any events are received. These can be accessed in Javascript through `globalValues.valueName` Values such as lastFollower, lastSub, lastGiftedSub, and chatMessages are set to that value's recent event(s).

```javascript
globalValues = {
	followerCount: 8,
	viewerCount: 4,
	lastFollower: {},
	lastSub: {},
	lastGiftedSub: {},
	chatMessages: [],
	streamerInfo: {
		kick: {
			username: 'henryk89',
			slug: 'henryk89',
			profile_picture: 'https://files.kick.com/images/user/4063326/profile_image/conversion/9a52aab8-71ed-4434-bc8b-1f7ae450bfb3-fullsize.webp',
			user_id: '4063326',
			channel_id: '3984507',
			chatroom_id: '3974112',
			bio: 'Developer of KickBot (kickbot.app). Contact me on Discord @ henry.',
			subscriber_badges: []
		}
	},
	currentStream: {
		is_live: false,
		title: '',
		started_at: ''
	}
}
```

## Example widget

An example follow goal widget is shown below. This widget uses events, global values, and user variables. This widget is available in the widget library and as a template when creating a new widget with the name "Simple follower goal".

#### HTML

```html
<div class="follower-goal-widget">
    <div class="goal-text"><span id="follower-goal-text"></span> <span id="current-followers"></span>/<span id="goal-followers"></span></div>
    <div class="progress-bar">
        <div class="progress"></div>
    </div>
</div>
```

#### CSS

```css
:root {
    --background-color: #100c28;
    --outline-color: #fff;
    --text-color: #fff;
    --bar-background-color: #3d3957;
    --bar-color: #57ee00;
    --text-size: 42px;
    --border-radius: 25px;
    --progress-height: 42px;
}

body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    margin: 0;
    font-family: Arial, sans-serif;
    background: #121212;
}

.follower-goal-widget {
    width: 98%;
    font-weight: bold;
    background-color: var(--background-color);
    border: 3px solid var(--outline-color);
    border-radius: var(--border-radius);
    padding: 12px;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.goal-text {
    color: var(--text-color);
    font-size: var(--text-size);
    white-space: nowrap;
    margin-right: 16px;
    padding-left: 8px;
}

.progress-bar {
    background-color: var(--bar-background-color);
    border-radius: var(--border-radius);
    overflow: hidden;
    flex-grow: 1;
    height: var(--progress-height);
    padding-right: 4px;
}

.progress {
    background-color: var(--bar-color);
    height: 100%;
    width: 0;
    transition: width 0.3s ease;
    border-radius: var(--border-radius);
}
```

#### Javascript

```javascript
document.addEventListener('widgetEvent', handleWidgetEvent);

const eventHandlers = {
    followEvent: function(event) {
        updateFollowerGoal(event.data.followers_count);
    },
    unfollowEvent: function(event) {
        updateFollowerGoal(event.data.followers_count);
    },
    followerCountUpdatedEvent: function(event) {
        updateFollowerGoal(event.data.followersCount);
    }
};

function handleWidgetEvent(event) {
    const eventName = event.detail.event_name;
    if (eventHandlers[eventName]) {
        eventHandlers[eventName](event.detail);
    }
}

function updateFollowerGoal(followersCount) {
    const followerGoalTextElem = document.getElementById('follower-goal-text');
    const progressElem = document.querySelector('.progress');
    const currentFollowersElem = document.getElementById('current-followers');
    let goalFollowers = userVariables.follower_goal.value;

    document.documentElement.style.setProperty('--outline-color', userVariables.outline_color.value);
    document.documentElement.style.setProperty('--background-color', userVariables.background_color.value);
    document.documentElement.style.setProperty('--text-color', userVariables.text_color.value);
    document.documentElement.style.setProperty('--bar-background-color', userVariables.bar_background_color.value);
    document.documentElement.style.setProperty('--bar-color', userVariables.bar_color.value);

    followerGoalTextElem.textContent = userVariables.follower_goal_text.value;
    currentFollowersElem.textContent = followersCount;
    const progressPercent = (followersCount / goalFollowers) * 100;
    progressElem.style.width = `${Math.min(progressPercent, 100)}%`;
}

// Initialize widget with global values and user variables
const goalFollowersElem = document.getElementById('goal-followers');
goalFollowersElem.textContent = userVariables.follower_goal.value;
updateFollowerGoal(globalValues.followerCount);
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.kickbot.com/dashboard/widgets/widget-builder-docs.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
