Panel A titled container with moods.
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 Web Component React Vue Angular
<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 18 Silver Hills Cafe $1,240.00
Aug 19 Falls City Supply $3,180.00
Aug 22 First Harrison deposit $12,400.00
Web Component React Vue Angular
Show code
<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 18 Silver Hills Cafe $1,240.00
Aug 19 Falls City Supply $3,180.00
Aug 22 First Harrison deposit $12,400.00
No activity in this period.
Web Component React Vue Angular
Show code
<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.
Web Component React Vue Angular
Show code
<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.
Web Component React Vue Angular
Show code
<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.
Web Component React Vue Angular
Show code
<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 Key Action Enter Space On the header of a collapsible panel, toggles the body.
API <sui-panel>Properties Property Attribute Type Default Description 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 | undefined Muted tabular text after the title (counts, balances).
Events Event Detail React Vue Description 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 Method Signature Description 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 Slot Description (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 type Why 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.
Every name above is listed with its value and its role in Foundations .
See also