UI widgets

Accordion

One engine, four behaviours. Headers are real buttons with aria-expanded, panels are labelled regions, and every panel height is a measured pixel value — no max-height guesswork, so long content never clips and short content never drags. Open panels park at height:auto, so they survive a resize. Press H to hide the control panel.

01

Classic — multi open

Every row keeps its own state, so a reader can leave several answers open side by side. Chevron flips 180°.

How do I add this to a page?

Drop in the script, mark up your rows, then call init on the wrapper. Nothing else loads and nothing else runs.

Does it need a build step?

No. It is one plain script with no imports, so it runs from a file path as happily as from a server.

The shared tween core is optional — the engine ships a small fallback and uses window.FX only when it finds it.

Can several panels stay open at once?

Yes. Set multi to true. Leave it false and opening one row closes the row that was open, with the same timing.

What about screen readers?

Each header becomes a real button carrying aria-expanded and aria-controls; each panel is a region labelled by its header. Closed panels are hidden, so their links stay out of the tab order.

02

Exclusive — one at a time

The FAQ pattern: opening a row closes the last one. Icon rotates 45° into a cross; switch it to the two-stroke plus-to-x morph in the panel.

Why measure the height in pixels?

A max-height guess either clips long content or spends half the tween animating empty space. Measuring the panel gives the tween an exact target, so the timing you set is the timing you get.

What happens when the window is resized?

When the open tween finishes, the panel is parked at height:auto. It then reflows with the page — text can rewrap to three lines and the row grows with it.

Can I drive it from code?

Call open(i), close(i) or toggle(i) with the row index. closeAll() and openAll() are there too, and onToggle fires on every state change.

Does it respect reduced motion?

Yes. When the system asks for less motion, rows snap open and shut with no tween, no FLIP and no flicker. The state and the aria wiring are unchanged.

03

Case list — rich rows

A portfolio row. Opening reveals a description and action pills, and the thumbnail strip FLIPs into a full-size media row. Media here is plain gradient boxes — swap them for images or video.

Northwind Ferries BrandingInterfaceMotion

A ferry operator with twelve routes and no shared visual language. We built one mark, one type scale and a booking flow that reads the same on a pier sign and on a phone.

View caseVisit site
Atlas Supply IdentityPackaging

Trade catalogue, 4,000 parts, printed twice a year. We cut the page count by a third by fixing the grid and the naming, then took the same system to the shelf.

View caseVisit site
Field & Foundry WebArt direction

A workshop that had outgrown its market stall. The site leads with the making, not the shop, and the shop follows.

View caseVisit site
04

Hover rows — open on approach

Rows open under the pointer: the pixel mark flickers, the title nudges right, a hairline draws in from the left and the panel reveals a media block. With no hover pointer — touch — the same rows fall back to tap, and the keyboard path is unchanged.

Identity systems Marks, type, guidelines

Six to ten weeks. A mark, a type scale and the rules that keep them honest once other people start using them.

Interface design Product screens, design systems

Screens first, tokens after. We draw the hard screens before the library, so the library answers real questions.

Motion Transitions, loaders, scroll

Motion with a job. Every move either shows where something came from or where it went.

Front-end build Zero-dependency, hand-written

No framework tax. Plain scripts, measured values, and a page that still works when a script fails to load.