Skip to content
ShineUI
Install

Panel

A titled container with moods.

<sui-panel>PanelsStable

About

Titled container with a sunken header, or a brand-toned header for ledger groups. collapsible turns the title into a disclosure button that hides the body. Reach for it when a section needs a heading and a meta line inside a card-like frame; use sui-accordion when several sections should take turns.

Usage

<sui-panel heading="1000 Operating Checking" meta="Balance $88,412.08"> <div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 18</span><span style="flex:1">Silver Hills Cafe</span><span style="font-feature-settings:'tnum'">$1,240.00</span></div> <div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 19</span><span style="flex:1">Falls City Supply</span><span style="font-feature-settings:'tnum'">$3,180.00</span></div> <div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 22</span><span style="flex:1">First Harrison deposit</span><span style="font-feature-settings:'tnum'">$12,400.00</span></div> </sui-panel> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-panel"); el.addEventListener("suiClose", (e) => console.log(e.detail)); </script>
import { SuiPanel } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return ( <SuiPanel heading="1000 Operating Checking" meta="Balance $88,412.08" onSuiClose={(e) => console.log(e.detail)}> <div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 18</span><span style="flex:1">Silver Hills Cafe</span><span style="font-feature-settings:'tnum'">$1,240.00</span></div> <div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 19</span><span style="flex:1">Falls City Supply</span><span style="font-feature-settings:'tnum'">$3,180.00</span></div> <div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 22</span><span style="flex:1">First Harrison deposit</span><span style="font-feature-settings:'tnum'">$12,400.00</span></div> </SuiPanel> ); }
<script setup> import { SuiPanel } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiPanel heading="1000 Operating Checking" meta="Balance $88,412.08" @sui-close="onChange"><div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 18</span><span style="flex:1">Silver Hills Cafe</span><span style="font-feature-settings:'tnum'">$1,240.00</span></div> <div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 19</span><span style="flex:1">Falls City Supply</span><span style="font-feature-settings:'tnum'">$3,180.00</span></div> <div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 22</span><span style="flex:1">First Harrison deposit</span><span style="font-feature-settings:'tnum'">$12,400.00</span></div></SuiPanel> </template>
import { Component } from "@angular/core"; import { SuiPanel } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiPanel], template: `<sui-panel heading="1000 Operating Checking" meta="Balance $88,412.08" [(collapsed)]="collapsed" (suiClose)="onChange($event)"><div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 18</span><span style="flex:1">Silver Hills Cafe</span><span style="font-feature-settings:'tnum'">$1,240.00</span></div> <div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 19</span><span style="flex:1">Falls City Supply</span><span style="font-feature-settings:'tnum'">$3,180.00</span></div> <div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 22</span><span style="flex:1">First Harrison deposit</span><span style="font-feature-settings:'tnum'">$12,400.00</span></div></sui-panel>`, }) export class Example { collapsed = false; onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

A plain panel is a titled container. meta is muted tabular text after the title.

Aug 18Silver Hills Cafe$1,240.00
Aug 19Falls City Supply$3,180.00
Aug 22First Harrison deposit$12,400.00
<sui-panel heading="1000 Operating Checking" meta="Balance $88,412.08"> <div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 18</span><span style="flex:1">Silver Hills Cafe</span><span style="font-feature-settings:'tnum'">$1,240.00</span></div> <div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 19</span><span style="flex:1">Falls City Supply</span><span style="font-feature-settings:'tnum'">$3,180.00</span></div> <div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 22</span><span style="flex:1">First Harrison deposit</span><span style="font-feature-settings:'tnum'">$12,400.00</span></div> </sui-panel>

Brand header

header-tone="brand" marks a ledger group. The header takes the brand ground and its inverse ink; the body keeps the surface.

Aug 18Silver Hills Cafe$1,240.00
Aug 19Falls City Supply$3,180.00
Aug 22First Harrison deposit$12,400.00
No activity in this period.
<div style="display:flex;flex-direction:column;gap:12px"> <sui-panel header-tone="brand" heading="1000 Operating Checking" meta="Opening balance $76,592.08"> <div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 18</span><span style="flex:1">Silver Hills Cafe</span><span style="font-feature-settings:'tnum'">$1,240.00</span></div> <div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 19</span><span style="flex:1">Falls City Supply</span><span style="font-feature-settings:'tnum'">$3,180.00</span></div> <div style="display:flex;justify-content:space-between;gap:16px;padding:5px 0;border-bottom:1px solid var(--hairline);font-size:13px"><span style="color:var(--ink-muted)">Aug 22</span><span style="flex:1">First Harrison deposit</span><span style="font-feature-settings:'tnum'">$12,400.00</span></div> </sui-panel> <sui-panel header-tone="brand" heading="1200 Accounts Receivable" meta="Opening balance $90,740.00"> <span style="font-size:13px;color:var(--ink-muted)">No activity in this period.</span> </sui-panel> </div>

Collapsible

The header becomes a disclosure button and the body unmounts while collapsed. Clicks inside header-end run their own action and leave the panel alone.

Falls City Supply Co. EIN 35-1882210. 410 Pearl Street, New Albany, IN 47150. Suspense account 9999. Rounding to the nearest cent.
<div style="display:flex;flex-direction:column;gap:12px"> <sui-panel id="pnl-open" collapsible heading="Company details" meta="3 fields"> <sui-button slot="header-end" variant="outline" size="sm" label="Edit"></sui-button> <span style="font-size:13px;color:var(--ink-muted)">Falls City Supply Co. EIN 35-1882210. 410 Pearl Street, New Albany, IN 47150.</span> </sui-panel> <sui-panel id="pnl-shut" collapsible collapsed heading="Advanced posting rules" meta="Defaults in use"> <span style="font-size:13px;color:var(--ink-muted)">Suspense account 9999. Rounding to the nearest cent.</span> </sui-panel> </div>

Driving it from script

collapsed is mutable and reflects. Writing it from script hides or shows the body and emits nothing; only the header emits.

Falls City Supply Co. EIN 35-1882210. Indiana sales tax registered. Kentucky registration is pending.
<div style="display:flex;flex-direction:column;gap:10px"> <div style="display:flex;gap:8px"> <sui-button id="pnl-fold" variant="outline" size="sm" label="Collapse both"></sui-button> <sui-button id="pnl-unfold" variant="outline" size="sm" label="Expand both"></sui-button> </div> <sui-panel class="pnl-driven" collapsible heading="Company details" meta="3 fields"> <span style="font-size:13px;color:var(--ink-muted)">Falls City Supply Co. EIN 35-1882210.</span> </sui-panel> <sui-panel class="pnl-driven" collapsible heading="Tax settings" meta="2 to review"> <span style="font-size:13px;color:var(--ink-muted)">Indiana sales tax registered. Kentucky registration is pending.</span> </sui-panel> </div>
// setup(root): properties and events setup(root) { const panels = root.querySelectorAll('.pnl-driven'); root.querySelector('#pnl-fold').addEventListener('click', () => { panels.forEach((p) => { p.collapsed = true; }); }); root.querySelector('#pnl-unfold').addEventListener('click', () => { panels.forEach((p) => { p.collapsed = false; }); }); }

Refusing a collapse

The header emits suiOpen after expanding and a cancelable suiClose with reason trigger before collapsing, so a host can keep an edited body on screen. The header is a button carrying aria-expanded and aria-controls.

Idle.
<div style="display:flex;flex-direction:column;gap:10px"> <sui-panel id="pnl-events" collapsible heading="Invoice notes" meta="Draft"> <sui-toggle-switch id="pnl-dirty" size="sm" label="Pretend this body has unsaved edits"></sui-toggle-switch> </sui-panel> <span id="pnl-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#pnl-log'); const dirty = root.querySelector('#pnl-dirty'); const panel = root.querySelector('#pnl-events'); panel.addEventListener('suiClose', (e) => { if (dirty.checked) { e.preventDefault(); log.textContent = `Collapse refused (${e.detail}). The body keeps the unsaved edits.`; return; } log.textContent = `Collapsed (${e.detail}).`; }); panel.addEventListener('suiOpen', () => { log.textContent = 'Expanded.'; }); }

Keyboard

KeyAction
Enter SpaceOn the header of a collapsible panel, toggles the body.

API

<sui-panel>

Properties

PropertyAttributeTypeDefaultDescription
collapsedcollapsedbooleanfalseWhether the body is hidden. Seeds the state (the reference defaultCollapsed) and tracks it afterwards; writing it from script emits nothing.
collapsiblecollapsiblebooleanfalseAdds the chevron and turns the title area into a disclosure button with aria-expanded and aria-controls. The body's height closes over the base duration, with the chevron, and the body unmounts once it has closed. Reflected.
headerToneheader-tone"brand" | undefinedGround of the header band: brand paints it on the brand fill under the inverse ink, for ledger groups and grouped sections; unset leaves the sunken surface header. The word the rails and the toolbar use for the ground they paint. Reflected.
headingheadingstring''Panel title (the reference title prop; renamed for the native attribute).
metametastringstring | undefinedMuted tabular text after the title (counts, balances).

Events

EventDetailReactVueDescription
suiCloseSuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger"onSuiClose@sui-closeFires before the header collapses the body; detail is 'trigger' (the header is the only way this element closes itself). Cancelable: preventDefault() leaves the panel open, so a host can keep unsaved edits on screen that unmounting would throw away. It does not bubble.
suiOpenvoidonSuiOpen@sui-openFires after the header expands the body. It does not bubble.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the disclosure button of a collapsible panel. Does nothing on a panel that does not collapse, which has no tab stop of its own.

Slots

SlotDescription
(default)Panel body (14/16px padding).
header-endContent at the end of the header row, after the meta text: a small action or a status.

Types

SuiCloseReason typeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own.
No members.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also