Containment
Collapsible
A single disclosure: a header you press and the block it shows or hides.
import { Collapsible } from 'omaris' Examples
Basic
One header, one body. bind:open holds the state; the body grows to its own height and the chevron turns with it.
Karada, Street 62, house 14 — ring twice.
The courier calls from 07xx when they are ten minutes away.
<script lang="ts"> import { Collapsible, Text } from 'omaris'; let open = $state(false);</script><div class="w-full max-w-md"> <Collapsible bind:open variant="outlined" title="Delivery details" supportingText="Tiger Express · today, 2–6 pm" > <div class="flex flex-col gap-2"> <Text variant="body-md">Karada, Street 62, house 14 — ring twice.</Text> <Text variant="body-sm" tone="muted"> The courier calls from 07xx when they are ten minutes away. </Text> </div> </Collapsible></div> States
A leading icon and a count in end for a panel that says how much it holds; filled when it is a thing of its own; disabled with the reason in the supporting line.
No refunds yet.
<script lang="ts"> import { Badge, Collapsible, Switch, Text } from 'omaris'; let open = $state(true); let settings = $state({ spicy: true, vegan: false, delivery: true });</script><div class="flex w-full max-w-md flex-col gap-4"> <Collapsible bind:open variant="filled" title="Filters"> {#snippet icon()} ... {/snippet} {#snippet end()} <Badge size="sm">{Object.values(settings).filter(Boolean).length}</Badge> {/snippet} <div class="flex flex-col gap-3"> <Switch bind:checked={settings.spicy} label="Spicy dishes" /> <Switch bind:checked={settings.vegan} label="Vegan only" /> <Switch bind:checked={settings.delivery} label="Delivers now" /> </div> </Collapsible> <Collapsible variant="outlined" disabled title="Refund history" supportingText="Available once the order is delivered" > <Text variant="body-md" tone="muted">No refunds yet.</Text> </Collapsible></div> Custom trigger
Any button can be the trigger: trigger hands you aria-expanded, aria-controls and the toggle to spread on it. Here, the "More options" under a form's one required field.
<script lang="ts"> import { Button, Collapsible, Input } from 'omaris'; let open = $state(false);</script><div class="flex w-full max-w-sm flex-col gap-2"> <Input label="Restaurant name" value="Beit Baghdad" /> <Collapsible bind:open classes={{ inner: 'flex flex-col gap-4 px-0 pt-2' }}> {#snippet trigger(props)} <Button variant="text" size="sm" {...props}> {open ? 'Fewer options' : 'More options'} <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" class={[ 'transition-[rotate] duration-300 ease-emphasized motion-reduce:transition-none', open && 'rotate-180' ]} > <path d="m6 9 6 6 6-6" stroke-linecap="round" stroke-linejoin="round" /> </svg> </Button> {/snippet} <Input label="Slug" value="beit-baghdad" /> <Input label="WhatsApp number" value="0770 123 4567" /> </Collapsible></div> Overridden
Every part is reachable: class for the root, classes for the trigger, chevron and body. headingLevel makes the trigger a real heading for a disclosure that titles a section.
<script lang="ts"> import { Collapsible } from 'omaris';</script><div class="w-full max-w-md"> <Collapsible open headingLevel={3} variant="outlined" title="Release notes" supportingText="v2.4.0" class="rounded-shape-2xl border-primary" classes={{ trigger: 'bg-primary-container text-primary-container-foreground', supporting: 'text-primary-container-foreground/80', chevron: 'text-primary-container-foreground', inner: 'pt-4 font-mono text-xs' }} > Faster menus, Kurdish plurals, and a lighter spinner. </Collapsible></div> When to use it
Use it for
- One block of detail behind a header — the delivery details on an order, the raw payload under a log line, the refund history on a receipt.
- "More options" under a form's required fields: the fields most people never touch, one press away.
triggertakes your ownButtonfor it. - A filter panel that folds away on a phone, with a count in
endthat says how much is applied while it is closed. - Open state that lives somewhere else:
openis bindable, so it can come from the URL, from storage, or from a toggle elsewhere on the page.
Not for
- A set of sections read one after another → Accordion, which keeps the headers in one list, moves between them with the arrows, and can close the others when one opens.
- Peers where exactly one is visible → Tabs.
- Content that should float over the page instead of pushing it down → Popover, or Sheet on a phone.
- Sections of navigation → Navigation Drawer.
Do
- Say what is inside in the header: "Delivery details", not "More". Use
supportingTextfor the one fact worth reading without opening it. - Pick the surface to match what is around it:
plaininside a card,outlinedamong other blocks,filledwhen it is a thing of its own. - Use
disabledwith the reason insupportingTextrather than hiding a section that cannot open yet. - Set
headingLevelwhen the disclosure titles a section of the page, so it shows up in a screen reader's list of headings.
Don't
- Put a required field, or an error, in a closed body. A validation message nobody can see is a form nobody can submit.
- Stack three collapsibles in a column; that is an Accordion.
- Put a button or a link in
titleorend— the header is already a button. - Nest one inside another more than once. Two levels of folding is where people stop finding things.
Quick reference
variant plain(default)outlinedfilled
API
Collapsible
A single disclosure: a header you press and the block it shows or hides.
The one-panel sibling of Accordion, with the same motion — the body rides a grid row going 0fr → 1fr, which is the one way to transition to a content-derived height without measuring it, so nothing jumps when the content changes while it is open. The chevron turns with it on the same curve, and the body fades in a beat behind so text never appears half clipped.
The header is either the built-in one — title, supportingText, an icon and an end slot, with the chevron — or your own element through trigger, which is handed the ARIA and the handler to spread on it:
A closed body is inert, so nothing inside it can take focus or be read out while it is out of sight.
import { Collapsible } from 'omaris' <Collapsible bind:open> {#snippet trigger(props)} <Button variant="text" {...props}>{open ? 'Show less' : 'Show all 12'}</Button> {/snippet} …</Collapsible> Parts
Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.
root- No description in the source yet.
trigger- No description in the source yet.
icon- Leading icon in the built-in trigger.
headline- No description in the source yet.
title- No description in the source yet.
supporting- No description in the source yet.
chevron- No description in the source yet.
wrapper- The animated wrapper; its single grid row is what opens.
content- Clips the body while the row is shorter than it.
inner- No description in the source yet.
Props
open bindableDefaults to false
boolean Whether the body is showing. Bindable.
onOpenChange (open: boolean) => void Called whenever it opens or closes from the trigger.
title string Header text for the built-in trigger.
supportingText string A second line under title — "3 filters applied".
variant Defaults to 'plain'
CollapsibleVariant plain flush on the page, outlined a hairline card, filled a tonal one.
plain- Flush on the page — no surface of its own.
outlined- A hairline card, for a disclosure that sits among other things.
filled- A tonal card, for a disclosure that is a thing of its own.
disabled Defaults to false
boolean The trigger does nothing and reads as unavailable. The body keeps its state.
headingLevel 2 | 3 | 4 | 5 | 6 Wraps the built-in trigger in a heading of this level, for a disclosure that titles a section of the page. Leave unset for one that doesn't.
class string classes CollapsibleClasses Per-part Tailwind overrides. class still covers the root.
icon Snippet Leading icon in the built-in trigger.
end Snippet Trailing content before the chevron — a count, a badge.
trigger Snippet<[CollapsibleTriggerProps]> Your own trigger in place of the built-in one. Spread the props onto a button — they carry aria-expanded, aria-controls and the toggle.
children Snippet The body.
id required string aria-expanded required boolean aria-controls required string onclick required (event: MouseEvent) => void