Draw in the interface with a mod changedplugins/mods/interface
Nearest release: v2.1.312, published an hour before upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.
Upstream edited this page at 27 Oct 2026 20:08 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 30 Oct 2026 18:07 UTC.
Upstream edited
Recorded here
Lines+132added
Lines−132removed
From line
177
where the diff opens
First seen
1 Oct 2026
this site's first read of the page
Recorded edits29to this page, all time
The whole hunk
from line 177, old and new numbered
/
from line 177
177177
178178The pane and the band are empty until a mod fills them. Select a tab to see what each one is and how to draw in it:
179179
180**Pane**
180> [tab] Pane
181>
182> A pane is a sidebar beside the transcript in a wide fullscreen terminal, or a framed region above the prompt otherwise. With several panes open, each gets a tab that shows its title.
183>
184> A pane appears when your mod calls `$.ui.open` with an `id` you choose, as in `$.ui.open({ id: 'hello-tabs' })`. [Open a pane at the right time](#open-a-pane-at-the-right-time) covers the other fields and when a pane waits for a wider terminal.
185>
186> To draw in your pane, filter on `{ component: 'Pane' }` and check that `e.requestId` is your `id`.
181187
182A pane is a sidebar beside the transcript in a wide fullscreen terminal, or a framed region above the prompt otherwise. With several panes open, each gets a tab that shows its title.
188> [tab] Band above the prompt
189>
190> The band is a strip directly above the prompt input. It's always there, and every mod shares it.
191>
192> Your hook returns a tree to show something in the band, or `next(e)` to show nothing. A tree replaces what the mods [after yours](https://code.claude.com/docs/en/plugins/mods/events.md#the-order-mods-run-in) draw there. To keep theirs, put the result of `await next(e)` among the children of a [`Box`](#build-a-tree-from-elements) in your tree.
193>
194> To draw in the band, filter on `{ component: 'AbovePrompt' }`.
183195
184A pane appears when your mod calls `$.ui.open` with an `id` you choose, as in `$.ui.open({ id: 'hello-tabs' })`. [Open a pane at the right time](#open-a-pane-at-the-right-time) covers the other fields and when a pane waits for a wider terminal.
185
186To draw in your pane, filter on `{ component: 'Pane' }` and check that `e.requestId` is your `id`.
187
188**Band above the prompt**
189
190The band is a strip directly above the prompt input. It's always there, and every mod shares it.
191
192Your hook returns a tree to show something in the band, or `next(e)` to show nothing. A tree replaces what the mods [after yours](https://code.claude.com/docs/en/plugins/mods/events.md#the-order-mods-run-in) draw there. To keep theirs, put the result of `await next(e)` among the children of a [`Box`](#build-a-tree-from-elements) in your tree.
193
194To draw in the band, filter on `{ component: 'AbovePrompt' }`.
195
196196### Change what Claude Code already draws
197197
198198Claude Code draws most of its interface itself: messages, tool call rows, the spinner, and more. Each of those parts is a render site too, so a mod can restyle or replace it. To change one, filter your `ui.render` hook on its name from this table:
from line 210
210210
211211At a site Claude Code already draws, your hook can change a detail, replace the drawing, or leave it alone. Select a tab to see each one applied to the spinner. The examples read a `calls` variable that another hook counts, as in the [tutorial mod](https://code.claude.com/docs/en/plugins/mods/create.md#write-a-mod-yourself).
212212
213**Change a detail**
213> [tab] Change a detail
214>
215> To keep Claude Code's drawing and change one part of it, pass `next` a copy of the event with changed `props`. This hook changes the text after the spinner's word:
216>
217> ```javascript theme={null}
218> on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
219> // Keep Claude Code's spinner, and change the text after its word
220> return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
221> })
222> ```
223>
224> The spinner keeps its animation and its word, and your text follows the word:
225>
226> ```text theme={null}
227> Thinking · tool calls: 2…
228> ```
214229
215To keep Claude Code's drawing and change one part of it, pass `next` a copy of the event with changed `props`. This hook changes the text after the spinner's word:
230> [tab] Replace the drawing
231>
232> To draw something of your own in the site's place, return a tree and don't call `next`. This hook draws one line of text where the spinner would be:
233>
234> ```javascript theme={null}
235> on('ui.render', { component: 'Spinner' }, async ($, e) => {
236> const { Text } = $.ui.resolve(e)
237> // No call to next, so this line is drawn in the spinner's place
238> return Text({ children: ['Claude has made ' + calls + ' tool calls'] })
239> })
240> ```
241>
242> While Claude works, your line shows and Claude Code's spinner doesn't:
243>
244> ```text theme={null}
245> Claude has made 2 tool calls
246> ```
216247
217```javascript theme={null}
218on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
219 // Keep Claude Code's spinner, and change the text after its word
220 return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
221})
222```
248> [tab] Leave it alone
249>
250> To leave the site as Claude Code draws it, return `next(e)`. A hook often does that for some events and not others. This hook leaves the spinner alone until there's a call to count:
251>
252> ```javascript theme={null}
253> on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
254> // Nothing to show yet, so pass the event on unchanged
255> if (calls === 0) return next(e)
256> return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
257> })
258> ```
259>
260> Before the first tool call, the spinner looks the way it does without the mod:
261>
262> ```text theme={null}
263> Thinking…
264> ```
223265
224The spinner keeps its animation and its word, and your text follows the word:
225
226```text theme={null}
227Thinking · tool calls: 2…
228```
229
230**Replace the drawing**
231
232To draw something of your own in the site's place, return a tree and don't call `next`. This hook draws one line of text where the spinner would be:
233
234```javascript theme={null}
235on('ui.render', { component: 'Spinner' }, async ($, e) => {
236 const { Text } = $.ui.resolve(e)
237 // No call to next, so this line is drawn in the spinner's place
238 return Text({ children: ['Claude has made ' + calls + ' tool calls'] })
239})
240```
241
242While Claude works, your line shows and Claude Code's spinner doesn't:
243
244```text theme={null}
245Claude has made 2 tool calls
246```
247
248**Leave it alone**
249
250To leave the site as Claude Code draws it, return `next(e)`. A hook often does that for some events and not others. This hook leaves the spinner alone until there's a call to count:
251
252```javascript theme={null}
253on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
254 // Nothing to show yet, so pass the event on unchanged
255 if (calls === 0) return next(e)
256 return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
257})
258```
259
260Before the first tool call, the spinner looks the way it does without the mod:
261
262```text theme={null}
263Thinking…
264```
265
266266At these sites, `next(e)` returns a reference to Claude Code's drawing, `{ type: 'engine', ref }`, unless a mod that runs after yours returned a tree of its own. To change what's in that drawing, pass `next` a copy of the event with different props, as the **Change a detail** tab does. You can return the reference as it is, or place it in a `Box` beside elements of your own:
267267
268268```javascript theme={null}
from line 353
353353
354354Select a tab to see each of the most common elements and how the terminal draws it:
355355
356**Text**
356> [tab] Text
357>
358> `Text` draws a string, with optional styling such as `bold` and `color`:
359>
360> ```javascript theme={null}
361> Text({ children: ['This is the first tab.'] })
362> ```
363>
364> ```text theme={null}
365> This is the first tab.
366> ```
357367
358`Text` draws a string, with optional styling such as `bold` and `color`:
368> [tab] Box
369>
370> `Box` arranges what's inside it, in a row or a column. This one puts a button and a line of text side by side, two columns apart:
371>
372> ```javascript theme={null}
373> Box({
374> flexDirection: 'row',
375> columnGap: 2,
376> children: [
377> Button({ key: 'more', label: 'Add one', onPress: addOne }),
378> Text({ children: ['Count: 0'] }),
379> ],
380> })
381> ```
382>
383> ```text theme={null}
384> [ Add one ] Count: 0
385> ```
359386
360```javascript theme={null}
361Text({ children: ['This is the first tab.'] })
362```
387> [tab] Button
388>
389> `Button` is a control the user can press. It runs your `onPress` callback. With `plain: true` it has no brackets and shows its hotkey:
390>
391> ```javascript theme={null}
392> Button({ key: 'more', label: 'Add one', onPress: addOne })
393> Button({ key: 'tab-one', label: 'One', hotkey: '1', plain: true, onPress: showTabOne })
394> ```
395>
396> ```text theme={null}
397> [ Add one ]
398> 1: One
399> ```
363400
364```text theme={null}
365This is the first tab.
366```
367
368**Box**
369
370`Box` arranges what's inside it, in a row or a column. This one puts a button and a line of text side by side, two columns apart:
371
372```javascript theme={null}
373Box({
374 flexDirection: 'row',
375 columnGap: 2,
376 children: [
377 Button({ key: 'more', label: 'Add one', onPress: addOne }),
378 Text({ children: ['Count: 0'] }),
379 ],
380})
381```
382
383```text theme={null}
384[ Add one ] Count: 0
385```
386
387**Button**
388
389`Button` is a control the user can press. It runs your `onPress` callback. With `plain: true` it has no brackets and shows its hotkey:
390
391```javascript theme={null}
392Button({ key: 'more', label: 'Add one', onPress: addOne })
393Button({ key: 'tab-one', label: 'One', hotkey: '1', plain: true, onPress: showTabOne })
394```
395
396```text theme={null}
397[ Add one ]
3981: One
399```
400
401**Input**
402
403`Input` is a text field. It runs your `onSubmit` callback with the text when the user presses Enter:
404
405```javascript theme={null}
406Input({
407 key: 'new-note',
408 label: 'Note',
409 placeholder: 'Type a note and press Enter',
410 value: '',
411 submitLabel: 'add',
412 onSubmit: addNote,
413})
414```
415
416```text theme={null}
417Note: Type a note and press Enter
418```
419
420Set `rows` to 2 or more for [a field that holds several lines](https://code.claude.com/docs/en/plugins/mods/gallery.md#input).
401> [tab] Input
402>
403> `Input` is a text field. It runs your `onSubmit` callback with the text when the user presses Enter:
404>
405> ```javascript theme={null}
406> Input({
407> key: 'new-note',
408> label: 'Note',
409> placeholder: 'Type a note and press Enter',
410> value: '',
411> submitLabel: 'add',
412> onSubmit: addNote,
413> })
414> ```
415>
416> ```text theme={null}
417> Note: Type a note and press Enter
418> ```
419>
420> Set `rows` to 2 or more for [a field that holds several lines](https://code.claude.com/docs/en/plugins/mods/gallery.md#input).
421421
422422The [interface gallery](https://code.claude.com/docs/en/plugins/mods/gallery.md) has samples and screenshots of most elements. This table lists every element:
423423
No line in this hunk matches that.