Collapsible
The collapsible expands and collapses a single panel of content from a trigger, like extra details behind a "show more". For a set of stacked panels where opening one closes the others, use an accordion.
<div class="flex w-full gap-3">
<span class="avatar">
<img
class="avatar-image"
src="https://images.unsplash.com/photo-1750390200282-bf7f669a9946?w=640&h=640&fit=facearea&facepad=3.2&auto=format&q=100"
alt="Leila Navarro"
/>
<span class="avatar-fallback">LN</span>
</span>
<div class="flex min-w-0 flex-col items-start gap-1">
<p class="text-sm">
<span class="font-medium">Leila Navarro</span>
<span class="text-muted-foreground">· 2h</span>
</p>
<p class="text-sm">
Just shipped the new onboarding flow. Huge thanks to everyone who helped
with the design reviews!
</p>
<button
id="collapsible-demo-trigger"
class="group btn btn-ghost btn-sm -ml-2.5 text-muted-foreground"
type="button"
>
<svg class="icon-start transition-transform group-aria-expanded:rotate-180" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
<span class="group-aria-expanded:hidden">Show 2 replies</span>
<span class="hidden group-aria-expanded:inline">Hide replies</span>
</button>
<div class="collapsible" data-sp-toggle="#collapsible-demo-trigger">
<div class="ml-[3.5px] flex flex-col gap-4 border-l pt-2 pl-4">
<div class="flex gap-3">
<span class="avatar">
<img
class="avatar-image"
src="https://images.unsplash.com/photo-1752486268240-0507bb1ebc7e?w=640&h=640&fit=facearea&facepad=2.6&auto=format&q=100"
alt="Camille Laurent"
/>
<span class="avatar-fallback">EW</span>
</span>
<div class="flex flex-col gap-1">
<p class="text-sm">
<span class="font-medium">Camille Laurent</span>
<span class="text-muted-foreground">· 1h</span>
</p>
<p class="text-sm">Congrats! The new flow feels so much faster.</p>
</div>
</div>
<div class="flex gap-3">
<span class="avatar">
<img
class="avatar-image"
src="https://images.unsplash.com/photo-1750390200293-92d5a788d3a2?w=640&h=640&fit=facearea&facepad=3&auto=format&q=100"
alt="Theo Marchetti"
/>
<span class="avatar-fallback">MC</span>
</span>
<div class="flex flex-col gap-1">
<p class="text-sm">
<span class="font-medium">Theo Marchetti</span>
<span class="text-muted-foreground">· 45m</span>
</p>
<p class="text-sm">Love the progress indicators. Great work!</p>
</div>
</div>
</div>
</div>
</div>
</div>Usage
Add the collapsible class to the panel and point it at its trigger with data-sp-toggle. The trigger's aria-expanded follows the state, which is the styling hook for rotating chevrons or swapping labels.
<!-- This is the trigger -->
<button id="my-trigger" class="btn btn-outline">Show more</button>
<!-- This is the panel -->
<div class="collapsible" data-sp-toggle="#my-trigger">
<div class="collapsible-content">Hidden until expanded.</div>
</div>Examples
Show more
Reveal the rest of a long text in place; the trigger swaps its label against the expanded state.
<div class="flex w-full flex-col items-start gap-2">
<h4 class="text-sm font-medium">About this project</h4>
<p class="text-sm text-muted-foreground">
Starting Point UI is an open-source component library for Tailwind CSS,
inspired by shadcn/ui. It gives you the same beautiful design system as
plain HTML classes, so it works in any project.
</p>
<div class="collapsible" data-sp-toggle="#collapsible-more-trigger">
<div class="flex flex-col gap-2 text-sm text-muted-foreground">
<p>
Components render correctly from markup and CSS before any JavaScript
loads, and the optional script layer adds behavior like this collapsible
on top.
</p>
<p>
Everything is themeable with CSS variables, and plain utility classes
always win over component styles, so you can override anything inline.
</p>
</div>
</div>
<button
id="collapsible-more-trigger"
class="group btn btn-link btn-sm px-0"
type="button"
>
<span class="group-aria-expanded:hidden">Show more</span>
<span class="hidden group-aria-expanded:inline">Show less</span>
</button>
</div>Initially open
Author the expanded class on the panel to start it open, and mirror aria-expanded="true" onto the trigger so state-driven styling, like a rotated chevron, paints correctly before JavaScript loads.
<nav class="sidebar-menu mx-auto w-full max-w-64">
<div class="sidebar-menu-item">
<button
id="collapsible-open-trigger"
class="group sidebar-menu-button"
type="button"
aria-expanded="true"
>
<svg class="transition-transform group-aria-expanded:rotate-90" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>
<span>Documents</span>
</button>
<div
class="collapsible expanded"
data-sp-toggle="#collapsible-open-trigger"
>
<ul class="sidebar-menu-sub">
<li class="sidebar-menu-sub-item">
<button class="sidebar-menu-sub-button w-full" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/></svg>
<span>resume.pdf</span>
</button>
</li>
<li class="sidebar-menu-sub-item">
<button class="sidebar-menu-sub-button w-full" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/></svg>
<span>invoice.pdf</span>
</button>
</li>
<li class="sidebar-menu-sub-item">
<button class="sidebar-menu-sub-button w-full" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/></svg>
<span>notes.md</span>
</button>
</li>
</ul>
</div>
</div>
</nav>Nested
Collapsibles nest, so a file tree is triggers and panels all the way down. The sidebar menu classes give it a ready-made look, indent line included.
<nav class="sidebar-menu mx-auto w-full max-w-64">
<div class="sidebar-menu-item">
<button
id="ct-src"
class="group sidebar-menu-button"
type="button"
aria-expanded="true"
>
<svg class="transition-transform group-aria-expanded:rotate-90" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>
<span>src</span>
</button>
<div class="collapsible expanded" data-sp-toggle="#ct-src">
<ul class="sidebar-menu-sub">
<li class="sidebar-menu-sub-item">
<button
id="ct-components"
class="group sidebar-menu-sub-button w-full"
type="button"
aria-expanded="true"
>
<svg class="transition-transform group-aria-expanded:rotate-90" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>
<span>components</span>
</button>
<div class="collapsible expanded" data-sp-toggle="#ct-components">
<ul class="sidebar-menu-sub">
<li class="sidebar-menu-sub-item">
<button class="sidebar-menu-sub-button w-full" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/></svg>
<span>button.tsx</span>
</button>
</li>
<li class="sidebar-menu-sub-item">
<button class="sidebar-menu-sub-button w-full" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/></svg>
<span>card.tsx</span>
</button>
</li>
</ul>
</div>
</li>
<li class="sidebar-menu-sub-item">
<button class="sidebar-menu-sub-button w-full" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/></svg>
<span>index.ts</span>
</button>
</li>
</ul>
</div>
</div>
</nav>RTL
Add dir="rtl" to any parent element, typically <html>, to render right-to-left. Flip directional chevrons with rtl:rotate-180. See the RTL guide for details.
<div id="collapsible-rtl-demo" class="flex w-full max-w-sm flex-col gap-2" dir="rtl">
<div class="flex items-center justify-between gap-4 px-4">
<h4 class="text-sm font-semibold" data-i18n="orderNumber">الطلب #4189</h4>
<button id="collapsible-rtl-trigger" class="btn btn-ghost btn-sm btn-icon" type="button" aria-expanded="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
<span class="sr-only" data-i18n="toggle">تبديل التفاصيل</span>
</button>
</div>
<div class="flex items-center justify-between rounded-md border px-4 py-2 text-sm">
<span class="text-muted-foreground" data-i18n="status">الحالة</span>
<span class="font-medium" data-i18n="shipped">تم الشحن</span>
</div>
<div class="collapsible expanded" data-sp-toggle="#collapsible-rtl-trigger">
<div class="flex flex-col gap-2">
<div class="rounded-md border px-4 py-2 text-sm">
<p class="font-medium" data-i18n="shippingAddress">عنوان الشحن</p>
<p class="text-muted-foreground" data-i18n="address">100 Market St, San Francisco</p>
</div>
<div class="rounded-md border px-4 py-2 text-sm">
<p class="font-medium" data-i18n="items">العناصر</p>
<p class="text-muted-foreground" data-i18n="itemsDescription">2x سماعات الاستوديو</p>
</div>
</div>
</div>
</div>
<script>
const translations = {
ar: { dir: "rtl", orderNumber: "الطلب #4189", toggle: "تبديل التفاصيل", status: "الحالة", shipped: "تم الشحن", shippingAddress: "عنوان الشحن", address: "100 Market St, San Francisco", items: "العناصر", itemsDescription: "2x سماعات الاستوديو" },
he: { dir: "rtl", orderNumber: "הזמנה #4189", toggle: "החלף פרטים", status: "סטטוס", shipped: "נשלח", shippingAddress: "כתובת משלוח", address: "100 Market St, San Francisco", items: "פריטים", itemsDescription: "2x אוזניות סטודיו" },
en: { dir: "ltr", orderNumber: "Order #4189", toggle: "Toggle details", status: "Status", shipped: "Shipped", shippingAddress: "Shipping address", address: "100 Market St, San Francisco", items: "Items", itemsDescription: "2x Studio Headphones" },
};
window.addEventListener("message", (e) => {
const t = translations[e.data?.lang];
if (e.data?.type !== "sp-language" || !t) return;
document.getElementById("collapsible-rtl-demo").dir = t.dir;
document.querySelectorAll("[data-i18n]").forEach((el) => {
el.textContent = t[el.dataset.i18n];
});
});
</script>Options
Collapsible
Set each option with its own data-sp-* attribute on the collapsible element.
<div class="collapsible" data-sp-toggle="#my-trigger"></div>JavaScript
Events
Lifecycle events fire on the panel; before* events are cancelable.
// Veto collapsing, e.g. while a form inside has unsaved changes.
panel.addEventListener("sp-beforecollapse", (e) => {
if (unsavedChanges) e.preventDefault();
});Methods
Get the instance with sp.collapsible(el), then call its methods:
const collapsible = sp.collapsible(document.querySelector("#my-panel"));
collapsible.expand();