Legacy skin API
The room.skin API of the legacy skin (basic) used by older chat rooms — jQuery DOM, menu bar, popups, nickname menu — and a mapping table for moving to the default skin.
Last updated October 17, 2026
On this page
The legacy skin (basic, shown in the admin page as "Legacy skin (old version)") is the skin older chat rooms use. New chat rooms are created with the default skin (basic-v2), and new features are added only to the default skin. This page lists room.skin for chat rooms that still use the legacy skin. For the default skin's API, see Skin UI API.
Page script U.chat(), events, room.action, channels (room.plugin), and the server API are the same in both skins. Only room.skin differs.
Check which skin is in use
The members of room.skin appear only after the skin builds the screen, so check inside after.create.
U.chat('my-room').on('after.create', function (room) {
if (room.skin.menubar) {
// Legacy skin
} else if (room.skin.menu) {
// Default skin
}
});
Both skins have userMenu, so don't use it to tell them apart. You can change the skin in the admin page Skin menu or with the <u-chat skin="basic-v2"> attribute. Features that don't depend on the skin, such as member integration, keep working after the change.
Principles
- The legacy skin works directly on the DOM inside the chat room iframe with jQuery (1.12). The skin's HTML structure and class names are not a fixed contract, and screens you modify may break when the skin changes.
- Do all registration inside
after.create. Before that, onlyroom.skin.argumentsandroom.skin.messagesexist. - Do not cover or remove the uchat logo or info display.
room.skin members
| Name | Description |
|---|---|
room.skin.arguments |
Skin settings (admin page skin settings + skin_arguments attribute + global Uchat_argument). The skin fills in color defaults |
room.skin.messages |
Skin UI text (current language) |
room.skin.window |
The chat room iframe's window. jQuery is room.skin.window.$ |
room.skin.document |
The chat room iframe's document |
room.skin.dom |
jQuery objects for the main areas. See dom below |
room.skin.menubar |
The menu bar under the input box. See menubar below |
room.skin.popup |
Popups (alert, confirm, input, select). See popup below |
room.skin.userMenu |
The menu that opens when you click a nickname. See userMenu below |
room.skin.scrollDown(force) |
Scrolls the chat to the bottom. Unless force is true, it does not scroll while the user has scrolled up |
dom
All of these are jQuery objects for elements inside the chat room iframe.
| Key | Area |
|---|---|
wrap |
The whole chat room (.wrap) |
top |
Top title area (.top) |
middle |
Chat and user list area (.middle) |
content |
Chat scroll area (.contentWrap) |
content_target |
Element that chat lines are appended to (.content) |
userList |
User list scroll area (.userListWrap) |
contour |
Divider between the user list and the chat (.contour) |
bottom |
Bottom input area (.bottom) |
inputContent |
Input box (.inputContent) |
menubar |
Menu bar (.menuBar) |
latest_chat |
Latest-message notice shown when scrolled up (.latest_chat) |
setting, settingBG |
Settings window and its background (.settingContent, .settingBackground) |
U.chat('my-room').on('after.create', function (room) {
var $ = room.skin.window.$;
var banner = $('<div/>').text('Event tonight at 8!').css({ padding: '6px', background: '#fff4cc' });
room.skin.dom.middle.before(banner);
$(room.skin.window).resize(); // If the height changed, let the skin re-layout
});
If you add elements and the height changes, call $(room.skin.window).resize() so the skin recalculates the chat area size. Insert user input with .text(), never .html().
menubar
Adds buttons to the menu bar under the input box.
U.chat('my-room').on('after.create', function (room) {
room.skin.menubar.add({
id: 'hello',
title: 'Send a greeting',
html: '<div>Hi</div>',
condition: function (room, item) {
return room.my.auth >= 1; // Hidden when false
},
onClick: function (room, item) {
room.action.send('Hello!');
},
});
});
| Key | Description |
|---|---|
id |
Required. Not registered if it's a duplicate. The button gets a menubar-<id> class |
title |
Tooltip on hover |
html |
Button content (HTML string). An empty <div> if omitted |
icon |
Background image URL |
condition(room, item) |
Not drawn if it returns exactly false |
onClick(room, item) |
On click. item.event is the click event |
beforeLoad(room, item), afterLoad(room, item) |
Before and after drawing. Not drawn if beforeLoad returns false |
beforeUnload(), afterUnload() |
Before and after removal |
item is the object you passed to add itself, and the jQuery object of the drawn button is stored in item.layout.
| Function | Description |
|---|---|
add(item) |
Registers. Returns true on success, false if the ID is missing or a duplicate |
refresh() |
Draws registered buttons according to condition |
remove(id) |
Removes the registration |
hide(), show(), toggle() |
Hide, show, or toggle the whole menu bar |
list |
Array of registered items |
addalone does not show anything on screen. The skin callsrefresh()every time you join, so buttons registered inafter.createappear on join. If you register after joining, callroom.skin.menubar.refresh()yourself.conditionis recalculated only onrefresh(). Ifconditionbecomesfalsefor a button already drawn, its registration is removed, and it does not come back even if the condition becomes true later. To hide and show a button by permission,toggle()theitem.layoutyou get inafterLoadinstead of usingcondition.remove(id)cannot remove the drawn button from the screen if the ID is not a number. When removing a button, also callitem.layout.remove().- Default button IDs include
scroll,bold,picture, andcall_admin.
popup
Shows popups inside the chat room. Only one popup is visible at a time; the rest wait in order. Every function returns a popup number, and you can close it with close(number). Results come through callbacks, not Promises.
var popup = room.skin.popup;
popup.confirm('Rock paper scissors', 'Accept the challenge?', function (ok) {
// true for OK, false for Cancel, null if closed by clicking outside
});
popup.prompt('Nickname', 'Enter a new nickname.', 'default', function (value) {
// The text entered, null if canceled
});
popup.select('Choose', 'Pick one.', { r: 'Rock', p: 'Paper', s: 'Scissors' }, function (key) {
// The chosen key (string), null if canceled
});
var id = popup.alert('Notice', 'Closes in 10 seconds.');
setTimeout(function () { popup.close(id); }, 10000);
| Function | Value passed to the callback |
|---|---|
alert(title, html, onOpen, onClose) |
onOpen(layer, pb, pu) is called when it opens, and onClose() when it closes. html is inserted as HTML, so escape user values |
confirm(title, subtitle, fn) |
true, false, or null if you click outside |
prompt(title, subtitle, value, fn) |
The text entered, null if canceled |
password(title, subtitle, value, fn) |
Same as prompt, with the input masked |
select(title, subtitle, list, fn) |
A key of list (object or array), null if canceled |
add(html, type, fn(layer, pb, pu), after) |
A custom popup. layer is { close(), centering() }, pb is the background, pu is the popup body (jQuery). after() is called on close |
close([number]) |
Closes that popup or removes it from the queue. Without a number, closes the popup currently shown |
closeByType(type) |
Closes all popups with the same type given to add |
- Pressing Enter in an input popup is the same as OK.
- When the browser window loses focus, the popup currently shown closes as if you clicked outside (
confirm,prompt, andselectreturnnull). In flows where the user opens another window and comes back, write your code expecting the result may benull.
userMenu
Adds items to the menu that opens when you click a nickname. The menu is rebuilt on every click, so condition is recalculated each time.
room.skin.userMenu.add({
id: 'profile',
text: 'View profile',
condition: function (room, data) {
return data.target !== room.my.nick; // Hidden on my own nickname
},
onClick: function (room, data) {
var user = room.user.get(data.target);
if (user && user.id) window.open('/profile/' + encodeURIComponent(user.id));
},
});
| Key | Description |
|---|---|
id |
Required. Not registered if it's a duplicate (false) |
text |
Item text |
condition(room, data) |
Hidden if it returns exactly false. data.target is the clicked nickname string |
onClick(room, data) |
On click. data is { event, id, target, messageUid } |
| Function | Description |
|---|---|
add(item) |
Adds one item |
group(left, right) |
Puts two items side by side on one row |
hr([{ condition }]) |
Divider. Its ID is always hr, so remove('hr') removes all dividers |
remove(id) |
Removes all items with that ID (including default items) |
list |
Array of registered items |
Default item IDs include info, whisper, call, ignore, un_ignore, report, invite, individual, mute, unmute, ban, ip, op, deop, freeze, unfreeze, clearlog, removeChat, and logout. To hide a default item, call remove(id). Even if you hide a menu item, the server still checks permissions.
Legacy-only events
before.user.list and after.user.list (data is { list }) fire when the legacy skin opens the user list and receives the full list. The default skin receives the user list in parts, so these events do not fire there. All other events are the same as in Events.
Moving to the default skin
| Task | Legacy skin | Default skin |
|---|---|---|
| Add a button | menubar.add({ id, title, html, condition(room, item), onClick(room, item) }) |
menu.add({ id, label, title, condition(ctx), onSelect(ctx) }) |
| Recalculate buttons | menubar.refresh() |
menu.resolver.refresh() |
| Nickname menu | userMenu.add({ id, text, onClick(room, data) }), data.target is the nickname |
userMenu.add({ id, label, onSelect(ctx) }), ctx.target is the user object |
| Hide default items | userMenu.remove('ignore') |
Filter out skin.ignore with userMenu.resolver.add(...) |
| Popups | Callbacks, such as popup.confirm(t, s, fn) |
Promises, such as dialog.confirm(t, s) |
| Insert an area | Direct DOM, such as dom.middle.before(...) |
mount.messageList({ position: 'before', mount(host) }) and others |
| Overlay on the chat | dom.content.append(...) + CSS |
mount.overlay({ layer, scope, mount(host) }) |
| Decorate nicknames and messages | Insert <style> via window.$ |
nickname.resolver, message.resolver |
| Scroll to bottom | scrollDown(force) |
scrollToBottom(force) |
The default skin does not support skin DOM, jQuery, or inserting <style>. For how to work after moving, start with Extending the skin UI.
Still not sure how it works? Ask your question on the contact board.