Extending the skin UI

Follow one example that adds a menu button to the default skin, takes input with a dialog, attaches an area above the chat, and cleans up with remove functions.

Last updated October 17, 2026

On this page

With the skin UI API (room.skin), you can add buttons, dialogs, and areas inside the default skin (basic-v2). This page walks through the flow by building a feature where "an admin clicks a button to show a banner above the chat". All functions and options are in Skin UI API.

Principles

  • The skin decides position, order, and stacking. Your code only touches the empty area (host) the skin hands you. Do not look up and modify the skin's internal DOM.
  • The id you pass when registering must be unique within each kind. A duplicate causes an error.
  • Register functions return a remove function. It is safe to call a remove function more than once.
  • This API exists only in the default skin. The legacy skin (basic) uses the Legacy skin API.

1. Wait until ready

The skin UI API is available only after the chat room screen is ready, so register inside after.create. For the preload code, see Async loading and multiple chat rooms.

U.chat('my-room').on('after.create', function (room) {
	var ui = room.skin;
	if (!ui.menu) return; // Stop if this is not the default skin
	// Register here
});

ui.menu.add adds a button to the input toolbar. If condition returns false, the button is hidden.

var removeMenu = ui.menu.add({
	id: 'my-banner',
	label: 'Banner',
	title: 'Show a banner above the chat',
	condition: function (ctx) { return ctx.room.my.auth >= 2; }, // Moderators and above only
	onSelect: function (ctx) { askBanner(); }
});

To add an item of the same shape to the menu that opens when you click a nickname, use ui.userMenu.add. There, ctx.target is the user object of the person you clicked. The server checks permissions even when a menu is hidden, so treat hiding only as a way to tidy the screen.

3. Take input with a dialog

Every ui.dialog.* function except open returns a Promise. prompt resolves to the entered string when confirmed, and null when canceled.

function askBanner() {
	ui.dialog.prompt('Banner', 'Enter the text to show.').then(function (text) {
		if (text) showBanner(text);
	});
}

alert, confirm (false when canceled), password, and select work the same way. If you need to close a dialog midway, use ui.dialog.open to get a handle with close. → Skin UI API

4. Attach an area

Pass a mount(host, ctx) function to ui.mount.*, and the skin creates an empty area in that spot and hands it to you as host. If mount returns a function, it is called once when the area goes away.

var removeBanner = null;

function showBanner(text) {
	if (removeBanner) removeBanner(); // Clean up the previous banner
	removeBanner = ui.mount.messageList({
		id: 'my-banner-area',
		position: 'before', // Above the message area
		mount: function (host, ctx) {
			var doc = host.ownerDocument;
			var box = doc.createElement('div');
			box.style.padding = '8px 12px';
			box.textContent = text;
			var close = doc.createElement('button');
			close.type = 'button';
			close.textContent = 'Close';
			close.addEventListener('click', function () { ctx.close(); });
			box.appendChild(close);
			host.appendChild(box);
			return function () { host.replaceChildren(); };
		}
	});
}
  • host is an element inside the chat room iframe. Create new elements with host.ownerDocument.
  • Insert text with textContent. Do not insert user input with innerHTML.
  • ctx.close() removes this registration. ctx.signal is aborted right before cleanup, so you can use it to clean up fetch calls or event listeners.

Besides messageList, there are the header, middle, userList, footer, wrap, composer, headerTitle, and toolbar spots, plus overlay, which stacks on top without pushing the layout. For the position values of each spot and the layer and scope of overlay, see Skin UI API.

5. Clean up with remove functions

Remove what you registered with the remove function you got back. When the chat room closes or is recreated, the skin cleans up all registrations, so you only need to clean up yourself when you turn off or change a feature.

function disableBanner() {
	if (removeBanner) removeBanner();
	removeMenu();
}

Removing something that is already removed does not cause an error. To register again with the same id, remove the old one first.

When the chat room is recreated by room.reload() or by re-inserting the tag, events registered with the chat room ID are removed too, so after.create does not fire again. To register again on recreation as well, register with U.chat('*') and check room.id. → Async loading and multiple chat rooms

Callbacks must be synchronous

The menu's condition and describe, a resolver's resolve, and message.extend's match are called while the skin is drawing the screen. These callbacks must be synchronous functions that return a value right away.

  • If condition, describe, resolve, or match returns a Promise, it is treated as an error and the result is ignored (a menu item will not show). Do not write them as async function.
  • If you need a value from your server, fetch it in advance and keep it in a variable. When the value changes, request a recalculation, for example with ui.menu.resolver.refresh().
  • The return value of onSelect is not used. Just start any async work inside onSelect after the button is clicked (as askBanner does above).

Full code

<script>U=window.U=window.U||{},U.events=U.events||[],U.chat=function(n){return{on:function(e,t){U.events.push([n,e,t])},off:function(e){for(var t=U.events.length;t>0;)U.events[--t][0]==n&&U.events[t][1]==e&&U.events.splice(t,1)}}};</script>
<script async src="https://client.uchat.io/uchat.js"></script>
<u-chat room="my-room" style="display:inline-block; width:100%; max-width:500px; height:500px;"></u-chat>
<script>
U.chat('my-room').on('after.create', function (room) {
	var ui = room.skin;
	if (!ui.menu) return;
	var removeBanner = null;

	function showBanner(text) {
		if (removeBanner) removeBanner();
		removeBanner = ui.mount.messageList({
			id: 'my-banner-area',
			position: 'before',
			mount: function (host, ctx) {
				var doc = host.ownerDocument;
				var box = doc.createElement('div');
				box.style.padding = '8px 12px';
				box.textContent = text;
				var close = doc.createElement('button');
				close.type = 'button';
				close.textContent = 'Close';
				close.addEventListener('click', function () { ctx.close(); });
				box.appendChild(close);
				host.appendChild(box);
				return function () { host.replaceChildren(); };
			}
		});
	}

	ui.menu.add({
		id: 'my-banner',
		label: 'Banner',
		title: 'Show a banner above the chat',
		condition: function (ctx) { return ctx.room.my.auth >= 2; },
		onSelect: function () {
			ui.dialog.prompt('Banner', 'Enter the text to show.').then(function (text) {
				if (text) showBanner(text);
			});
		}
	});
});
</script>

The banner shows only on the screen of the person who clicked the button. To show it to everyone, send the text over a channel and call showBanner on the receiving side. → Sending and receiving real-time data

Business name UCHAT · CEO 김경욱 · Business registration no. 141-05-32832
Address 경기도 파주시 와석순환로 61 · Mail-order business registration no. 2013-경기파주-4485호
Phone 010-8677-8633 · Email support@uchat.io

ⓒ 2013–2026 UCHAT. All rights reserved.