Skip to content
ShineUI
Install

Spoiler

Long text, short leash.

<sui-spoiler>MiscStable

About

Clamps long content to a line count and reveals the rest behind a "Show more" toggle. Use for memos, notes and descriptions that would otherwise push the rest of the page down.

Usage

<sui-spoiler lines="2"> Fall supplies order for the New Albany dock. Confirm pallet counts with receiving before 3 PM. The vendor asked us to split the freight across two invoices, so match both against PO-2211 when the feed lands. Terms are Net 30 from the later of the two invoice dates. </sui-spoiler> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-spoiler"); el.addEventListener("suiClose", (e) => console.log(e.detail)); </script>
import { SuiSpoiler } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return ( <SuiSpoiler lines="2" onSuiClose={(e) => console.log(e.detail)}> Fall supplies order for the New Albany dock. Confirm pallet counts with receiving before 3 PM. The vendor asked us to split the freight across two invoices, so match both against PO-2211 when the feed lands. Terms are Net 30 from the later of the two invoice dates. </SuiSpoiler> ); }
<script setup> import { SuiSpoiler } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiSpoiler lines="2" @sui-close="onChange">Fall supplies order for the New Albany dock. Confirm pallet counts with receiving before 3 PM. The vendor asked us to split the freight across two invoices, so match both against PO-2211 when the feed lands. Terms are Net 30 from the later of the two invoice dates.</SuiSpoiler> </template>
import { Component } from "@angular/core"; import { SuiSpoiler } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiSpoiler], template: `<sui-spoiler lines="2" [(open)]="open" (suiClose)="onChange($event)">Fall supplies order for the New Albany dock. Confirm pallet counts with receiving before 3 PM. The vendor asked us to split the freight across two invoices, so match both against PO-2211 when the feed lands. Terms are Net 30 from the later of the two invoice dates.</sui-spoiler>`, }) export class Example { open = false; onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

The toggle is a native button carrying aria-expanded and aria-controls for the clamped body. The clamp is visual only, so the full text stays in the accessibility tree in both states and aria-expanded describes the visible height.

Fall supplies order for the New Albany dock. Confirm pallet counts with receiving before 3 PM. The vendor asked us to split the freight across two invoices, so match both against PO-2211 when the feed lands. Terms are Net 30 from the later of the two invoice dates.
<div style="max-width:420px;font:460 13px/1.6 var(--font-sans);color:var(--ink)"><sui-spoiler lines="2">Fall supplies order for the New Albany dock. Confirm pallet counts with receiving before 3 PM. The vendor asked us to split the freight across two invoices, so match both against PO-2211 when the feed lands. Terms are Net 30 from the later of the two invoice dates.</sui-spoiler></div>

Line count and labels

lines sets the collapsed height. moreLabel and lessLabel replace the default wording, which is where a different register or language goes.

Fall supplies order for the New Albany dock. Confirm pallet counts with receiving before 3 PM. The vendor asked us to split the freight across two invoices, so match both against PO-2211 when the feed lands. Terms are Net 30 from the later of the two invoice dates. Fall supplies order for the New Albany dock. Confirm pallet counts with receiving before 3 PM. The vendor asked us to split the freight across two invoices, so match both against PO-2211 when the feed lands. Terms are Net 30 from the later of the two invoice dates.
<div style="display:grid;gap:20px;max-width:420px;font:460 13px/1.6 var(--font-sans);color:var(--ink)"> <sui-spoiler lines="1">Fall supplies order for the New Albany dock. Confirm pallet counts with receiving before 3 PM. The vendor asked us to split the freight across two invoices, so match both against PO-2211 when the feed lands. Terms are Net 30 from the later of the two invoice dates.</sui-spoiler> <sui-spoiler lines="4" more-label="Read the full memo" less-label="Collapse the memo">Fall supplies order for the New Albany dock. Confirm pallet counts with receiving before 3 PM. The vendor asked us to split the freight across two invoices, so match both against PO-2211 when the feed lands. Terms are Net 30 from the later of the two invoice dates.</sui-spoiler> </div>

Expanded on mount

open is mutable and reflected, so a parent can render the memo already expanded and bind the state. Writing open from outside emits nothing, because the parent already knows.

Fall supplies order for the New Albany dock. Confirm pallet counts with receiving before 3 PM. The vendor asked us to split the freight across two invoices, so match both against PO-2211 when the feed lands. Terms are Net 30 from the later of the two invoice dates.
<div style="max-width:420px;font:460 13px/1.6 var(--font-sans);color:var(--ink)"><sui-spoiler lines="2" open>Fall supplies order for the New Albany dock. Confirm pallet counts with receiving before 3 PM. The vendor asked us to split the freight across two invoices, so match both against PO-2211 when the feed lands. Terms are Net 30 from the later of the two invoice dates.</sui-spoiler></div>

Events

The toggle emits suiOpen after it expands and a cancelable suiClose before it collapses, with detail "trigger", the only way this element closes itself. Bind open to these two and a parent never goes stale.

Fall supplies order for the New Albany dock. Confirm pallet counts with receiving before 3 PM. The vendor asked us to split the freight across two invoices, so match both against PO-2211 when the feed lands. Terms are Net 30 from the later of the two invoice dates. Idle.
<div style="display:grid;gap:10px;max-width:420px;font:460 13px/1.6 var(--font-sans);color:var(--ink)"> <sui-spoiler id="sp-events" lines="2">Fall supplies order for the New Albany dock. Confirm pallet counts with receiving before 3 PM. The vendor asked us to split the freight across two invoices, so match both against PO-2211 when the feed lands. Terms are Net 30 from the later of the two invoice dates.</sui-spoiler> <span id="sp-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('#sp-log'); const el = root.querySelector('#sp-events'); el.addEventListener('suiOpen', () => { log.textContent = 'Expanded.'; }); el.addEventListener('suiClose', (e) => { log.textContent = `Collapsing (${e.detail}).`; }); }

Holding it open

preventDefault on suiClose keeps the memo expanded. Use it while something below the fold still needs reading, such as an unacknowledged notice.

Fall supplies order for the New Albany dock. Confirm pallet counts with receiving before 3 PM. The vendor asked us to split the freight across two invoices, so match both against PO-2211 when the feed lands. Terms are Net 30 from the later of the two invoice dates. Expand it, then try to collapse it.
<div style="display:grid;gap:10px;max-width:420px;font:460 13px/1.6 var(--font-sans);color:var(--ink)"> <sui-spoiler id="sp-veto" lines="2">Fall supplies order for the New Albany dock. Confirm pallet counts with receiving before 3 PM. The vendor asked us to split the freight across two invoices, so match both against PO-2211 when the feed lands. Terms are Net 30 from the later of the two invoice dates.</sui-spoiler> <span id="sp-veto-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Expand it, then try to collapse it.</span> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#sp-veto-log'); const el = root.querySelector('#sp-veto'); let acknowledged = false; el.addEventListener('suiClose', (e) => { if (acknowledged) return; e.preventDefault(); acknowledged = true; log.textContent = 'Held open once. Collapse again and it closes.'; }); }

Keyboard

KeyAction
TabLands on the Show more control below the clamped text; it reads aria-expanded and controls the body.
Enter SpaceToggles the body. Opening emits suiOpen; closing emits a cancelable suiClose with reason trigger and stays open when a listener cancels it.

API

<sui-spoiler>

Properties

PropertyAttributeTypeDefaultDescription
lessLabelless-labelstringstring | undefinedToggle label while expanded.
lineslinesnumber3Visible line count while collapsed. The clamp is visual only: the full text stays in the accessibility tree in both states.
moreLabelmore-labelstringstring | undefinedToggle label while collapsed.
openopenbooleanfalseWhether the content is expanded (the reference defaultOpen). Mutable and reflected: the toggle flips it and a parent can bind it. A write from outside emits nothing.

Events

EventDetailReactVueDescription
suiCloseSuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger"onSuiClose@sui-closeFires before the toggle collapses the content; detail is 'trigger', the only way this element closes itself. Cancelable: preventDefault() keeps it expanded. It does not bubble.
suiOpenvoidonSuiOpen@sui-openFires after the toggle expands the content, so a parent binding open stays current. It does not bubble, as no open-state event does.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the Show more toggle. Does nothing while the content fits and there is no toggle.

Slots

SlotDescription
(default)The long content to clamp.

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