Accordions

Native <details> / <summary>. Open / close, keyboard and screen-reader semantics are the browser's; the framework JS only adds optional events and deep-linking.

Four classes: .accordion (wrapper), .accordion-item (each <details>), .accordion-heading (the <summary>), .accordion-panel (the content).

Keyboard

Accordion keyboard interaction
KeyAction
Tab / Shift+TabMove to the next / previous <summary> (or link inside an open panel).
Enter / SpaceToggle the focused panel.

Markup

A shared name makes the group exclusive — opening one closes the rest (native, no script). Drop name to let several stay open. open sets the initial state.

Ancient survivors

Jellyfish predate dinosaurs and trees by hundreds of millions of years, and have ridden out every mass extinction since.

Fishy imposters

Not fish at all — invertebrate medusae in the phylum Cnidaria.

Watery beasts

Almost entirely water, with no brain, heart or bones.

Colours

A palette-family class on .accordion or a single .accordion-item themes the headings (needs $generate-accordion-modifiers). Any family in $colors works — not just these three.

Giant tentacles

A lion's mane jellyfish can trail tentacles longer than a blue whale.

Immortal jelly

Turritopsis dohrnii can revert to its polyp stage and start over.

No brain, no problem

A decentralised nerve net is enough to hunt, flee and mate.

Box jellyfish sting

Among the most venomous animals in the ocean — treat every sighting with caution.

Animation

The panel animates its block-size open and closed via interpolate-size: allow-keywords (scoped to .accordion) and ::details-content. Where those aren't supported the panel just snaps — still correct, only unanimated. Padding lives on .accordion-panel inside the clipped box, so a closed item is exactly zero height. prefers-reduced-motion removes the animation.

Deep linking

A link to a fragment inside a closed panel (<a href="#watery-beasts">) opens that item before scrolling — on load, on hashchange, and for same-page clicks. Chromium does this natively for <details> (including find-in-page); the script covers Firefox and Safari. Set --jf-scroll-offset if you have a sticky header.

Jump to "Watery beasts" — it's closed above; the link opens it.

There's also the text-fragment URL syntax — #:~:text=…, Chromium/Edge only for now — which scrolls to and highlights matching text directly, with no id needed. Chromium opens an ancestor <details> for this natively too, same as a normal fragment link:

Jump to the words "Almost entirely water" — inside the same closed panel, matched by text rather than id.

Events


    document.addEventListener("jfAccordionOpened", (e) => {
      e.detail; // { id: "watery-beasts", title: "Watery beasts", open: true }
    });
  

jfAccordionOpened / jfAccordionClosed fire on document for every toggle, with that detail shape — id is the <details> element's own id attribute (empty string if it has none), title comes from data-title on the <details> if set, else the <summary>'s text.

The same toggle also pushes to window.dataLayer (GTM), if it exists — a different shape, not a copy of the event detail:


    {
      event: "accordionOpened", // or "accordionClosed"
      accordionId: "#watery-beasts", // "#" + id; empty string if no id
      accordionTitle: "Watery beasts",
    }
  

Note the difference from the event detail above: accordionId is #-prefixed, there's no separate open key (it's encoded in event), and the keys are named differently.

Overriding

  • One accordion / a scoped rule — set the --jf-accordion-* custom properties (tabled below) on .accordion or a single .accordion-item.
  • Everywhere — the Sass variables below.

Variables

Sass variables

Accordions Sass variables
NameDefaultNotes
$generate-accordion-modifierstrueEmit the .{family} colour classes.
$accordion-padding-yget-space(xs)Block padding on both the <summary> and the panel.
$accordion-padding-xget-space(sm)Inline padding on both.
$accordion-heading-font-sizetoken(font-size-md)<summary> text size. A heading nested inside inherits it.
$accordion-heading-weightget-weight(bold)<summary> text weight.
$accordion-border-radiustoken(radius-sm)Rounds the group's outer corners only — items in the middle stay square so the stack reads as one block.

Custom properties

Accordions custom properties
NameDefaultNotes
--jf-accordion-heading-backgroundneutral / subtleSummary fill, closed and unhovered.
--jf-accordion-heading-color--jf-color-on-neutral-subtleSummary text, closed and unhovered.
--jf-accordion-heading-hover-backgroundneutral / mutedSummary fill when hovered or open — the same styling covers both.
--jf-accordion-heading-hover-color--jf-color-on-neutral-mutedSummary text when hovered or open.
--jf-accordion-border-colorfalls back to --jf-border-colorNot emitted by default. A .{family} modifier sets it to that family's muted role.
--jf-icona chevronThe ::after disclosure mask, rotated 180° when open.