Components
Accordion
A section that opens in place to reveal more.
Use it for detail that most people do not need — FAQs, specifications, terms — where opening should not take you off the page.
Watch out. Don't hide anything essential behind one. If everyone needs to read it, it should already be visible.
Sub-brandTheme
Liverendered from the shipped recipe
Style
ghost default
panel
States
closed
hover
open
open-hover
Code
The HTML for the live example above is on the page - use your browser's inspector, or copy the CSS here and the markup from the preview. Load post-ui.css once per site and this component works anywhere.
/* ============================================================================
POST UI System — components
HAND-WRITTEN. Not generated: this file encodes the RULES the components follow,
which is not the same thing as whatever the Figma canvas currently looks like.
The contract it implements (variant axes + reasoning) is synced in components.json.
Import order: tokens.css -> type.css -> components.css
NAMING. Every class is `post-` prefixed. This ships into Webflow projects where
`.button`, `.field`, `.label` or `.text-body` are exactly the classes a site
already has, and an unprefixed collision would silently restyle real content.
Block `post-x` · modifier `post-x--y` · state `is-*`.
THE FOUR RULES EVERYTHING BELOW OBEYS
1. Tokens are named by scale, never by consumer — space/m, stroke/m, icon-size/m
and text/md all mean "the middle step".
2. Strokes are altitude, not component — s detail · m component · l section.
3. Optical padding — outer padding is uniform, the LABEL carries space/xs left and
right, and the gap is 0. Text ends up inset by outer+xs while an icon sits at
outer, which is correct: a glyph carries internal whitespace and text does not.
One recipe covers icon-left, icon-right, both, or neither.
4. Control height comes from the ICON TIER, never from text. Every control label
carries min-height = its icon-size token. Without it the icon becomes the
tallest child and toggling an optional icon grows the control — a chip jumped
28 -> 32px exactly this way. With it, toggling an icon changes width only.
currentColor is load-bearing. Outline/ghost controls bind their label fill AND
their stroke to the same `action` token, so the border is written as
`border-color: currentColor` and one colour change moves both.
============================================================================ */
/* ---- accordion ---- */
/* ghost = transparent with a section rule, content added IN PLACE.
panel = inverted: an action-coloured plane with on-action ink.
The + rotates 45deg to an x — one glyph, not two, because the icon describes what
happens to the page. */
/* Same fixed-slot rule as Select and Search Bar: Figma pins every variant to a FIXED
width with the rule, header, title and body all set to FILL. The parent decides the
width; the TITLE takes up the slack inside it. Without a declared width the accordion
shrink-wrapped to its own heading wherever it was a flex/grid child, so a row of them
came out ragged — each one only as wide as its title — and the top rule, which is
supposed to read as a section divider, stopped short. */
.post-accordion {
width: 100%;
box-sizing: border-box;
/* width:100% alone is not enough in a flex/grid parent: a flex item defaults to
min-width:auto, which refuses to shrink below its own content, so a long heading
still widened the column and a row of accordions came out uneven (190/194/210 for
three different titles). min-width:0 lets the slot win and the title wrap. */
min-width: 0;
border-top: var(--stroke-l) solid var(--border);
}
.post-accordion__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-m);
width: 100%;
box-sizing: border-box;
padding: var(--space-m);
border: none;
background: none;
cursor: pointer;
text-align: left;
font-family: var(--font-post);
font-size: var(--text-lg);
font-weight: 500;
line-height: 1;
color: var(--text);
}
/* Title and disclosure icon share ONE ink: text at rest, hover-ink on hover — they
are one affordance, so they must never diverge. Figma binds the ghost disclosure
glyph to `text`, NOT `action`; it was pinned to action here, which rendered a brand
blue + on a near-black title and split the one affordance in two.
The icon now sets no colour of its own — it inherits the head's, which is the
mechanism that makes "one ink" structurally true instead of two rules kept in sync. */
/* Title FILLs, glyph is FIXED — Figma's sizing modes, expressed as flex. min-width:0
lets a long title wrap instead of forcing the head wider than its slot; flex:0 0 auto
stops the glyph being squeezed when it does. */
.post-accordion__title { flex: 1 1 auto; min-width: 0; }
.post-accordion__head .post-icon {
flex: 0 0 auto;
width: var(--icon-size-l);
height: var(--icon-size-l);
transition: transform 0.15s ease;
}
/* Hover means two DIFFERENT things per style, which is why this is scoped rather than
global. On ghost, hover moves the INK (there is no plane to move). On panel the plane
darkens and the ink stays inverted — see the panel block below.
Unscoped, this rule was `.post-accordion.is-hover .post-accordion__head` at (0,3,0) and
out-specified the panel's own (0,2,0) ink rule, so a hovered panel painted its title in
hover-ink on an action-hover plane — the same colour, i.e. an invisible heading. */
.post-accordion:not(.post-accordion--panel) .post-accordion__head:hover,
.post-accordion:not(.post-accordion--panel).is-hover .post-accordion__head { color: var(--hover-ink); }
.post-accordion.is-open .post-accordion__head .post-icon { transform: rotate(45deg); }
.post-accordion__body { display: none; padding: 0 var(--space-m) var(--space-m); font-size: var(--text-md); color: var(--text); line-height: 1.2; }
.post-accordion.is-open .post-accordion__body { display: block; }
/* panel is the inverted plane. Its outline is bound to on-action in Figma, not action —
an action-on-action border is invisible, which is why the panel read as a flat blue
block with no edge. currentColor expresses it: the plane sets color:on-action once and
the border follows, which is the same rule outline buttons use. */
.post-accordion--panel {
background: var(--action);
color: var(--on-action);
border: var(--stroke-m) solid currentColor;
}
.post-accordion--panel .post-accordion__head,
.post-accordion--panel .post-accordion__body { color: var(--on-action); }
/* Hover darkens the plane in BOTH states. This was gated on .is-open, so a closed panel
did not respond to the pointer at all — Figma has Style=panel/State=hover on
action-hover with no open requirement. */
.post-accordion--panel:hover,
.post-accordion--panel.is-hover { background: var(--action-hover); }