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
| Key | Action |
|---|---|
| Tab / Shift+Tab | Move to the next / previous <summary> (or link inside an open panel). |
| Enter / Space | Toggle 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
| Name | Default | Notes |
|---|---|---|
| $generate-accordion-modifiers | true | Emit the .{family} colour classes. |
| $accordion-padding-y | get-space(xs) | Block padding on both the <summary> and the panel. |
| $accordion-padding-x | get-space(sm) | Inline padding on both. |
| $accordion-heading-font-size | token(font-size-md) | <summary> text size. A heading nested inside inherits it. |
| $accordion-heading-weight | get-weight(bold) | <summary> text weight. |
| $accordion-border-radius | token(radius-sm) | Rounds the group's outer corners only — items in the middle stay square so the stack reads as one block. |
Custom properties
| Name | Default | Notes |
|---|---|---|
| --jf-accordion-heading-background | neutral / subtle | Summary fill, closed and unhovered. |
| --jf-accordion-heading-color | --jf-color-on-neutral-subtle | Summary text, closed and unhovered. |
| --jf-accordion-heading-hover-background | neutral / muted | Summary fill when hovered or open — the same styling covers both. |
| --jf-accordion-heading-hover-color | --jf-color-on-neutral-muted | Summary text when hovered or open. |
| --jf-accordion-border-color | falls back to --jf-border-color | Not emitted by default. A .{family} modifier sets it to that family's muted role. |
| --jf-icon | a chevron | The ::after disclosure mask, rotated 180° when open. |