Draw in the interface with a mod changedplugins/mods/interface
Nearest release: v2.1.321, 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 9 Nov 2026 21:00 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 9 Nov 2026 21:07 UTC.
Upstream edited
Recorded here
Lines+87added
Lines−0removed
From line
193
where the diff opens
First seen
1 Oct 2026
this site's first read of the page
Recorded edits30to this page, all time
### Place a row in the transcript ### Put a backdrop behind the conversation
The whole hunk
from line 193, old and new numbered
/
from line 193
193193>
194194> To draw in the band, filter on `{ component: 'AbovePrompt' }`.
195195
196A mod can also [place a row of its own in the transcript](#place-a-row-in-the-transcript).
197
196198### Change what Claude Code already draws
197199
198200Claude 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 346
344346* **The question dialog**: at `AskUserQuestion`, the panel keeps its own question card and draws your elements above it. Props you pass to `next` don't change the card.
345347* **Prompt suggestions**: `$.prompt.suggest` resolves with `isShown: false` and shows nothing, because the panel keeps the prompt box's dim suggestion text for Claude's own suggestions.
346348* **Text limits**: the panel cuts a status line at 200 characters or two rows, and a toast at 500 characters.
349
350### Place a row in the transcript
351
352A row is a line your mod adds to the transcript in the terminal, after the latest message. It stays at that spot as the conversation goes on, and Claude doesn't see it. `$.ui.place` requires Claude Code v2.1.314 or later.
353
354**1. Place the row**
355
356Call [`$.ui.place`](https://code.claude.com/docs/en/plugins/mods/reference#mods-api-methods) with an `id`. The `id` is a name you make up for the row, such as `build-status`. It can be 1 to 64 letters, digits, `_`, or `-`, and each row your mod has in the transcript needs its own.
357
358**2. Draw the row**
359
360Draw the row at the [`Placed`](https://code.claude.com/docs/en/plugins/mods/reference#render-sites) render site.
361
362This example adds a `/build-status` command. When the user runs the command, the mod adds a row that shows `Build: passing`. The text is fixed to keep the example short, so a real mod would draw its own result:
363
364```javascript theme={null}
365export function register(on) {
366 on('session.start', async ($, e, next) => {
367 await $.command.register({ name: 'build-status', description: 'Show the build status' })
368 return next(e)
369 })
370
371 on('command.run', { command: 'build-status' }, async ($) => {
372 const placed = await $.ui.place({ id: 'build-status' })
373 // Where nothing draws the row, reply with text instead
374 return placed.isPlaced ? {} : { text: 'Build: passing' }
375 })
376
377 on('ui.render', { component: 'Placed' }, async ($, e, next) => {
378 // This hook runs for every mod's rows, and another mod can use the same id
379 if (e.props.plugin !== $.plugin.name || e.props.id !== 'build-status') return next(e)
380 const { Text } = $.ui.resolve(e)
381 return Text({ children: ['Build: passing'] })
382 })
383}
384```
385
386To try the example, put this code in a [mod](https://code.claude.com/docs/en/plugins/mods/create.md). In a terminal session, run `/build-status` at the Claude Code prompt. `Build: passing` appears as a row at the end of the transcript.
387
388The call doesn't always show a row, and a row can go away later, so handle these cases in your mod:
389
390* **The result**: `{ isPlaced: true, requestId }` when the terminal draws the row. Otherwise `isPlaced` is `false` and `reason` says why, as in `no attached surface draws placed rows yet`. One reason is the [limit on rows](https://code.claude.com/docs/en/plugins/mods/reference.md#limits) a mod can have at one time.
391* **A second call with the same `id`**: while the first row is still in the transcript, the call adds no row and resolves with that row's `requestId`. To change what the row shows, [redraw the row](#redraw-when-something-changes).
392* **When the row goes away**: when the conversation ends, when the user rewinds past the row, or, outside [fullscreen rendering](https://code.claude.com/docs/en/fullscreen.md), when the conversation is compacted. `--resume` doesn't bring the row back. To show the row again, call `$.ui.place` again.
393
394### Put a backdrop behind the conversation
395
396> [!NOTE]
397>
398> `$.theme.set` and `$.theme.clear` replace `$.ui.backdrop`, which Claude Code v2.1.319 removed. Change `$.ui.backdrop({ id: 'your-backdrop' })` to `$.theme.set({ backdrop: { id: 'your-backdrop' } })`, and `$.ui.backdrop(undefined)` to `$.theme.clear()`, which removes any mod's backdrop.
399
400To put a backdrop behind the conversation in the Desktop app or the mobile app, call [`$.theme.set`](https://code.claude.com/docs/en/plugins/mods/reference#mods-api-methods) with a `backdrop`, then draw the backdrop at the [`Backdrop`](https://code.claude.com/docs/en/plugins/mods/reference#render-sites) render site. To take the backdrop off, call `$.theme.clear()`. The terminal draws no backdrop. `$.theme.set` and `$.theme.clear` require Claude Code v2.1.319 or later.
401
402This mod sets a backdrop when the session starts, draws the backdrop, and adds a `/plain` command that takes the backdrop off:
403
404```javascript theme={null}
405export function register(on) {
406 // Neither a reload of this mod nor --resume keeps the backdrop, so set it here
407 on('session.start', async ($, e, next) => {
408 await $.theme.set({ backdrop: { id: 'night-sky', colorScheme: 'dark' } })
409 await $.command.register({ name: 'plain', description: 'Take the backdrop off' })
410 return next(e)
411 })
412
413 on('ui.render', { component: 'Backdrop' }, async ($, e, next) => {
414 // This hook runs for every mod's backdrop, and another mod can use the same id
415 if (e.props.plugin !== $.plugin.name || e.props.id !== 'night-sky') return next(e)
416 const { Box, Text } = $.ui.resolve(e)
417 return Box({ backgroundColor: 'blue', children: [Text({ children: ['Night sky'] })] })
418 })
419
420 on('command.run', { command: 'plain' }, async ($) => {
421 const cleared = await $.theme.clear()
422 return { text: cleared.reason }
423 })
424}
425```
426
427To try the example, put this code in a [mod](https://code.claude.com/docs/en/plugins/mods/create.md) and start a session in the Desktop app or the mobile app. To take the backdrop off, run `/plain` at the Claude Code prompt. The transcript shows `the theme is cleared` after the mod's name, as in `night-sky: the theme is cleared` for a mod named `night-sky`.
428
429Check what `backdrop` takes, what the call's result tells you, and what other mods can change:
430
431* **The `backdrop` fields**: `id`, a [name](https://code.claude.com/docs/en/plugins/mods/reference.md#limits) you choose, is required, and the other fields are optional. `colorScheme` is `'light'` or `'dark'` and says whether your drawing is light or dark. For `glass`, see [the types for your build](https://code.claude.com/docs/en/plugins/mods/create.md#get-the-types-for-your-build).
432* **Whether the backdrop is drawn**: `$.theme.set` resolves with `isDrawn: true` when a Desktop or mobile app connected to the session says it draws backdrops. Otherwise `isDrawn` is `false` and `reason` says why.
433* **One backdrop for each session**: a `$.theme.set` call replaces the session's backdrop and `$.theme.clear()` removes that backdrop, whichever mod set it. Call `$.theme.clear()` only when the user asks for it.
347434
348435## Build a tree from elements
349436
No line in this hunk matches that.