import {
Button,
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
Text,
} from '@vega-ui/react';
<div>
<Collapsible defaultOpen>
<CollapsibleTrigger asChild>
<Button size='sm' variant='secondary' appearance='ghost'>Billing details</Button>- Browser support
- Widely available
- Chrome 111
- Edge 111
- Firefox 113
- Safari 15.4
- Safari on iOS 15.4
- Bundle size
- Oklab and OkLCh — comes from the design tokens
- :where()
Summary
Collapsible is a compound component for one expandable region with a trigger and content surface. It is the primitive disclosure pattern when grouped accordion behavior is not needed.
Imports
import {
Collapsible,
CollapsibleTrigger,
CollapsibleContent,
type CollapsibleProps,
} from '@vega-ui/react';Exported Types
CollapsiblePropsCollapsibleTriggerPropsCollapsibleContentProps
Minimal Composition
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@vega-ui/react';
export default function MinimalComposition() {
return (
<div style={{ width: '100%' }}>
<Collapsible>
<CollapsibleTrigger>Advanced billing options</CollapsibleTrigger>
<CollapsibleContent>
VAT settings, invoice recipients, and legal entity data.
</CollapsibleContent>
</Collapsible>
</div>Required Parts
Collapsible: root state ownerCollapsibleTrigger: expandable trigger surfaceCollapsibleContent: expandable content region
Optional Parts
- No extra public parts beyond the root trigger and content model
Composition Order
CollapsibleCollapsibleTriggerCollapsibleContent
Variants
- Uncontrolled versus controlled open state
- Default button trigger versus
asChildtrigger composition - Inline help, filters, and secondary details as product-level variants
Edge Cases
CollapsibleTriggercan render throughasChild, but custom semantics should still preserve a real trigger contract.onChangeHiddenreflects visibility lifecycle, which can differ from the open state during height transitions.- Dynamic content height should be tested with real content, not just short placeholders.
Common Mistakes
- Using
Collapsiblewhere groupedAccordionbehavior is actually needed. - Hiding essential information behind vague trigger labels.
- Treating
openandhiddenas the same lifecycle signal.
Examples
Basic
Basic: price breakdown under a total
An uncontrolled disclosure that keeps the total visible and hides the line items until asked. The trigger is a Button through asChild, so it keeps the button styles and still gets aria-expanded and aria-controls.
import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger, Text } from '@vega-ui/react';
const lines = [
{ label: 'Team plan · 8 seats × $12', amount: '$96.00' },
{ label: 'Extra storage · 200 GB', amount: '$20.00' },
{ label: 'Annual discount −10%', amount: '−$11.60' },
{ label: 'VAT 19%', amount: '$19.84' },
];
export default function PriceBreakdown() {
return (
<div style={{ display: 'grid', gap: 'var(--spacing-6)', width: '100%', maxWidth: 360 }}>Controlled/Stateful
Controlled/Stateful: advanced filters with a summary
The parent owns open and uses it for the trigger label. The filter values live next to it, so the summary under the block still says how many filters apply when the block is collapsed.
import { useState } from 'react';
import {
Button,
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
Label,
Text,
TextField,
TextFieldInput,
} from '@vega-ui/react';
Form/Integration
Form/Integration: optional company details
Hidden content is not disabled: its inputs would still be submitted and their required would still block the form. A <fieldset disabled={!open}> around the optional fields removes them from FormData and from validation while the block is closed.
import { FormEvent, useState } from 'react';
import {
Button,
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
Label,
TextField,
TextFieldInput,
} from '@vega-ui/react';
export default function InvoiceDetailsForm() {Layout/Overlay
Layout/Overlay: technical details in a dialog
The dialog explains the failure in plain words; the stack trace sits in a collapsed block with its own scroll area, so a long trace does not stretch the dialog.
import type { CSSProperties } from 'react';
import {
Button,
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
Dialog,
DialogClose,
DialogContent,
DialogTrigger,
Heading,
Icon,Error
Error: promo code with inline validation
The form lives inside the content. An unknown code shows an error linked with aria-describedby, and typing clears it. The applied discount is announced through a role='status' region that is always rendered.
import { FormEvent, useState } from 'react';
import {
Button,
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
HelperText,
Label,
Text,
TextField,
TextFieldInput,
} from '@vega-ui/react';Disabled
Disabled: trigger disabled by a parent fieldset
CollapsibleTrigger renders a native button, so a <fieldset disabled> disables it together with the inputs inside. The reason is linked to the fieldset with aria-describedby. If the block was open before locking, it stays open but read-only.
import { useState } from 'react';
import {
Button,
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
Label,
Text,
TextField,
TextFieldInput,
} from '@vega-ui/react';
Edge
Edge: stop background work once the content is hidden
onChangeHidden fires after the closing transition ends, not when the trigger is pressed. The log polls only while the content is shown and pauses when it is fully hidden.
import { useEffect, useState } from 'react';
import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger, Text } from '@vega-ui/react';
const routes = ['GET /api/health', 'POST /api/orders', 'GET /api/orders/10482', 'PATCH /api/users/42'];
export default function LiveLog() {
const [hidden, setHidden] = useState(true);
const [lines, setLines] = useState<string[]>([]);
useEffect(() => {
if (hidden) return;
const timer = setInterval(() => {Anatomy
Overview
Collapsible owns one expandable region and coordinates its trigger, content visibility, and hidden-state lifecycle. It is a compound component because the trigger and content depend on one shared root context.
Required Parts
Collapsible
Required. Owns open state, hidden-state lifecycle, and content ID coordination.
CollapsibleTrigger
Required. Toggles the region and carries aria-expanded plus aria-controls.
CollapsibleContent
Required. Renders the expandable region and manages the height-based visibility transition.
Optional Parts
Collapsible does not expose extra public child parts beyond the trigger and content pair.
Composition Order
CollapsibleCollapsibleTriggerCollapsibleContent
Valid Composition Patterns
import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger, Text } from '@vega-ui/react';
const lines = [
{ label: 'Team plan · 8 seats × $12', amount: '$96.00' },
{ label: 'Extra storage · 200 GB', amount: '$20.00' },
{ label: 'Annual discount −10%', amount: '−$11.60' },
{ label: 'VAT 19%', amount: '$19.84' },
];
export default function PriceBreakdown() {
return (
<div style={{ display: 'grid', gap: 'var(--spacing-6)', width: '100%', maxWidth: 360 }}>Invalid Composition Patterns
Content outside Collapsible
This breaks the shared ID, hidden state, and transition lifecycle.
Trigger semantics replaced by a non-interactive child
If asChild is used, the child still needs to behave like a real trigger.
Several related sections implemented as unrelated collapsibles
If the sections should coordinate, use Accordion instead of separate Collapsible roots.