GrCollapse
Collapses and expands additional content on demand.
Overview
When to take it
- there is a lot of content and not all of it is needed at once — questions and answers, settings, the details of a record;
- one section is open at a time —
accordioncloses the previous one; - the sections are needed by a blind user —
headingLevelsets a heading of the right level, and walking by headings works; - opening has to be intercepted —
beforeChangepostpones the expansion until a load or a confirmation.
When to take something else
| Need | Take |
|---|---|
| The sections are switched rather than expanded | GrTabs |
| The sections of a form follow one another | GrFormSection |
| The data is nested | GrTree |
| The content opens on top of the page | GrDialog / GrDrawer |
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrCollapse emptyText="No sections yet" v-model="value">
<GrCollapseItem name="billing" title="Billing"><span class="text-[var(--gr-muted-fg)]">Invoices go out on the 1st; the card ending 4242 is charged the same day.</span></GrCollapseItem>
<GrCollapseItem name="tax" title="Tax details"><span class="text-[var(--gr-muted-fg)]">VAT DE 293 847 561 · reverse charge applies.</span></GrCollapseItem>
<GrCollapseItem name="contacts" title="Billing contacts"><span class="text-[var(--gr-muted-fg)]">[email protected], [email protected]</span></GrCollapseItem>
</GrCollapse>Examples 4 each with a live demo and its code
Billing questions, several open at once
The model is an array of names, so the answers being compared stay open together. The page heading is
h2, andheading-levelmakes the questionsh3: a screen reader walks them by structure, while each trigger stays a button inside its heading, as the APG pattern asks.Billing questions
You pay for the new seats only for the days left in the billing period. The amount appears as a separate line on the next invoice, called «Proration».
We charge VAT when the company has no valid EU VAT number on file. Add the number in Settings → Billing, and the next invoice will be issued with reverse charge.
Visa, Mastercard and American Express, SEPA Direct Debit for euro accounts, and bank transfer on the yearly plan.
Monthly plans are not refunded for the current month. Yearly plans are refunded pro rata within the first 30 days.
An order, one section at a time
accordionkeeps a single name in the model, so opening Payment folds Items and the total stays in view.borderlessdrops the second frame inside the card,divideddraws the rules, and theextraslot puts a badge next to the trigger, not inside it.Order #4815€312.40 3- Halden desk, oak€249.00
- Desk lamp, black€39.90
- Cable tray, 80 cm€23.50
Tomorrow12 Harbour Street, 20457 Hamburg · DHL, before 12:00PaidVisa ending 4242 · 6 Oct, 09:14A filter panel that counts what is on
Each group shows in
extrahow many of its filters are on, so a folded section still tells the truth.expand-icon-position="start"puts the chevron where a list starts, and adisabledsection explains itself in its title.A section that will not fold with unsaved changes
before-changegets the section and the direction. Edit the company name and try to fold the section: the guard refuses and says why. Open «Invoice history» for the first time and the guard waits for the data before the section expands — a second click meanwhile is ignored.Exports all invoices, then deletes the workspace after 30 days.
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
disabled | boolean | undefined | false | Locks all the sections: the headers do not toggle. |
modelValuev-model | GrCollapseValue | GrCollapseValue[] | undefined | undefined | The open sections (`v-model`): in `accordion` — one value or `undefined`, otherwise an array. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The size of the sections. Unset — it comes from `GrConfigProvider`, otherwise `md`. |
headingLevel | 2 | 3 | 4 | 5 | 6 | undefined | undefined | The level of the section headings (`h2`…`h6`) — to fit the structure of the page. |
accordion | boolean | undefined | false | Only one section is open at a time: opening a new one closes the previous one. |
divided | boolean | undefined | undefined | Separators between the sections. `true` by default; globally — `componentDefaults`. |
borderless | boolean | undefined | undefined | No wrapping in `GrCard`: otherwise an accordion inside a card or a sidebar gets a second border and a second shadow. `false` by default. |
expandIconPosition | "end" | "start" | undefined | undefined | The side the chevron is on relative to the heading. |
beforeChange | GrCollapseBeforeChange | undefined | undefined | An async guard for toggling: it receives the section and the direction, `false` cancels. Until it has answered, a repeated click on the same section is ignored. |
empty | boolean | undefined | undefined | Whether it is empty. Unset — it is judged by the content: an accordion with no sections shows a placeholder instead of an empty frame. `false` suppresses the automatic detection — for instance when the sections arrive asynchronously and the text must not flash. |
emptyText | string | undefined | undefined | The text of the empty state. The `#empty` slot is stronger. |
Slots
| Slot | Type | Description |
|---|---|---|
default | any | The sections of the accordion (`GrCollapseItem`). |
empty | any | The content of the empty state instead of the default text. |
Events
| Event | Signature | Description |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: GrCollapseModelValue): void | — |
change | (event: "change", value: GrCollapseModelValue): void | — |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrCollapseProps, GrCollapseEmits, GrCollapseItemProps } from '@feugene/granularity/components/GrCollapse'Filedist/types/components/GrCollapse/GrCollapse.vue.d.ts
interface GrCollapseProps {
modelValue?: GrCollapseModelValue;
accordion?: boolean;
disabled?: boolean;
divided?: boolean;
borderless?: boolean;
headingLevel?: GrCollapseHeadingLevel;
expandIconPosition?: GrCollapseIconPosition;
beforeChange?: GrCollapseBeforeChange;
size?: GrComponentSize;
empty?: boolean;
emptyText?: string;
}interface GrCollapseEmits {
(e: 'update:modelValue', value: GrCollapseModelValue): void;
(e: 'change', value: GrCollapseModelValue): void;
}Filedist/types/components/GrCollapse/GrCollapseItem.vue.d.ts
interface GrCollapseItemProps {
name?: GrCollapseValue;
title?: string;
disabled?: boolean;
}Filedist/types/components/GrCollapse/config.d.ts
const grCollapseConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrCollapse">;Filedist/types/components/GrCollapse/defaults.d.ts
interface GrCollapseConfigurableProps {
size: GrComponentSize;
divided: boolean;
borderless: boolean;
expandIconPosition: GrCollapseIconPosition;
headingLevel: GrCollapseHeadingLevel;
}Filedist/types/components/GrCollapse/safelist.d.ts
const grCollapseSafelist: string[];Accessibility
APG pattern
accordion
Keys
Space / Enter↓ / ↑Home / End
Full contract of the package: docs/keyboard.md ↗
Usage from the library prose
The heading level
The heading of a section renders as an h3 tag, and headingLevel fits it to the
structure of the page: inside an <h4> section an accordion has to start with h5,
otherwise navigation by headings gets a gap in levels. The button stays inside the
heading — the APG requires that for an accordion.
The surface
borderless removes the GrCard wrapper: an accordion inside a card, a sidebar or
a filter panel would otherwise get a second border and a second shadow. divided
controls the separators between sections.
<GrCollapse v-model="open" borderless :heading-level="5" expand-icon-position="start">
<GrCollapseItem name="filters" title="Filters">
<template #extra>
<GrBadge size="sm">3</GrBadge>
</template>
…
</GrCollapseItem>
</GrCollapse>The #icon slot replaces the chevron, expandIconPosition moves it before the
heading. The #extra slot (a counter, a badge, a button) renders beside the
trigger rather than inside it: a <button> inside a <button> is invalid markup,
and axe catches it as nested-interactive.
A guard on switching
beforeChange(name, expanding) cancels the switch by returning false. The second
argument is where the section is heading, so that “save the changes?” is asked only
on collapsing. Until the guard has answered, a repeated click on the same heading is
ignored: otherwise two confirmations in a row would return the state to where it
started.
async function beforeChange(name: GrCollapseValue, expanding: boolean): Promise<boolean> {
if (expanding)
return true
return confirmDiscardChanges(name)
}The keyboard and nesting
The ↑/↓ arrows (in a ring) and Home/End walk only the headings of their
own accordion: a nested GrCollapse inside an expanded panel does not enter the
walk.
A collapsed panel is marked inert — neither Tab nor a screen reader enters it,
and at the same time (unlike hidden) the expansion animation is preserved.
The empty state
<GrCollapse>
<GrCollapseItem v-for="item in filtered" :key="item.name" v-bind="item" />
</GrCollapse>The filter found nothing — the accordion will show text instead of an empty frame itself: a frame with no content reads as a breakage rather than as “nothing yet”.
Emptiness is judged by the content of the slot, not by the length of your data, so
both a v-for over an empty array and a v-if that rendered nothing work. The
comments a v-if leaves behind and the line breaks from the template do not count
as content — otherwise the placeholder would never appear.
The text comes from the locale (gr.collapse.empty). It can be overridden in two
ways, from the simple to the general:
<GrCollapse empty-text="No sections" />
<GrCollapse>
<template #empty>
<GrEmptyState title="Nothing found" description="Loosen the filter" />
</template>
</GrCollapse>:empty="false" suppresses the automatic detection — that is needed when the
sections arrive asynchronously and the placeholder must not flash in the first
frame. :empty="true" shows it forcibly.
Props in this example