# 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.

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](/en/doc/3.4).

## Principles {#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](/en/doc/3.8).

## 1. Wait until ready {#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](/en/doc/2.2).

```js
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 {#menu}

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

```js
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 {#dialog}

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

```js
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](/en/doc/3.4)

## 4. Attach an area {#mount}

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.

```js
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](/en/doc/3.4).

## 5. Clean up with remove functions {#cleanup}

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.

```js
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](/en/doc/2.2)

## Callbacks must be synchronous {#sync-callbacks}

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 {#full-code}

```html
<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](/en/doc/2.4)
