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
idyou 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
});
2. Add a menu button
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(); };
}
});
}
hostis an element inside the chat room iframe. Create new elements withhost.ownerDocument.- Insert text with
textContent. Do not insert user input withinnerHTML. ctx.close()removes this registration.ctx.signalis aborted right before cleanup, so you can use it to clean upfetchcalls 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, ormatchreturns a Promise, it is treated as an error and the result is ignored (a menu item will not show). Do not write them asasync 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
onSelectis not used. Just start any async work insideonSelectafter the button is clicked (asaskBannerdoes 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
Still not sure how it works? Ask your question on the contact board.