GrCollapse

  • core
  • overlays
  • API · 11 props
  • ⌨ keys

Collapses and expands additional content on demand.

import { GrCollapse } from '@feugene/granularity/components/GrCollapse'

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 — accordion closes the previous one;
  • the sections are needed by a blind user — headingLevel sets a heading of the right level, and walking by headings works;
  • opening has to be intercepted — beforeChange postpones the expansion until a load or a confirmation.

When to take something else

NeedTake
The sections are switched rather than expandedGrTabs
The sections of a form follow one anotherGrFormSection
The data is nestedGrTree
The content opens on top of the pageGrDialog / GrDrawer

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

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, and heading-level makes the questions h3: 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

    accordion keeps a single name in the model, so opening Payment folds Items and the total stays in view. borderless drops the second frame inside the card, divided draws the rules, and the extra slot 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

    Tomorrow
    12 Harbour Street, 20457 Hamburg · DHL, before 12:00

    Paid
    Visa ending 4242 · 6 Oct, 09:14
  • A filter panel that counts what is on

    Each group shows in extra how 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 a disabled section explains itself in its title.

  • A section that will not fold with unsaved changes

    before-change gets 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

PropTypedefaultDescription
disabledboolean | undefinedfalseLocks all the sections: the headers do not toggle.
modelValuev-modelGrCollapseValue | GrCollapseValue[] | undefinedundefinedThe open sections (`v-model`): in `accordion` — one value or `undefined`, otherwise an array.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe size of the sections. Unset — it comes from `GrConfigProvider`, otherwise `md`.
headingLevel2 | 3 | 4 | 5 | 6 | undefinedundefinedThe level of the section headings (`h2`…`h6`) — to fit the structure of the page.
accordionboolean | undefinedfalseOnly one section is open at a time: opening a new one closes the previous one.
dividedboolean | undefinedundefinedSeparators between the sections. `true` by default; globally — `componentDefaults`.
borderlessboolean | undefinedundefinedNo 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" | undefinedundefinedThe side the chevron is on relative to the heading.
beforeChangeGrCollapseBeforeChange | undefinedundefinedAn 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.
emptyboolean | undefinedundefinedWhether 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.
emptyTextstring | undefinedundefinedThe text of the empty state. The `#empty` slot is stronger.

Slots

SlotTypeDescription
defaultanyThe sections of the accordion (`GrCollapseItem`).
emptyanyThe content of the empty state instead of the default text.

Events

EventSignatureDescription
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
import type { GrCollapseProps, GrCollapseEmits, GrCollapseItemProps } from '@feugene/granularity/components/GrCollapse'

Filedist/types/components/GrCollapse/GrCollapse.vue.d.ts

GrCollapsePropsinterface
interface GrCollapseProps {
    modelValue?: GrCollapseModelValue;
    accordion?: boolean;
    disabled?: boolean;
    divided?: boolean;
    borderless?: boolean;
    headingLevel?: GrCollapseHeadingLevel;
    expandIconPosition?: GrCollapseIconPosition;
    beforeChange?: GrCollapseBeforeChange;
    size?: GrComponentSize;
    empty?: boolean;
    emptyText?: string;
}
GrCollapseEmitsinterface
interface GrCollapseEmits {
    (e: 'update:modelValue', value: GrCollapseModelValue): void;
    (e: 'change', value: GrCollapseModelValue): void;
}

Filedist/types/components/GrCollapse/GrCollapseItem.vue.d.ts

GrCollapseItemPropsinterface
interface GrCollapseItemProps {
    name?: GrCollapseValue;
    title?: string;
    disabled?: boolean;
}

Filedist/types/components/GrCollapse/config.d.ts

grCollapseConfigconst
const grCollapseConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrCollapse">;

Filedist/types/components/GrCollapse/defaults.d.ts

GrCollapseConfigurablePropsinterface
interface GrCollapseConfigurableProps {
    size: GrComponentSize;
    divided: boolean;
    borderless: boolean;
    expandIconPosition: GrCollapseIconPosition;
    headingLevel: GrCollapseHeadingLevel;
}

Filedist/types/components/GrCollapse/safelist.d.ts

grCollapseSafelistconst
const grCollapseSafelist: string[];

Accessibility

APG pattern

accordion

Keys

Space / Enter↓ / ↑Home / End

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.

vue
<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.

ts
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
vue
<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:

vue
<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.