Accordion
Vertically stacked disclosure sections on bits-ui Accordion. Height springs open and closed via a gentle preset; reduced-motion collapses the transition to zero.
Usage
Bloom ships a tokens-first component library keyed on hex design tokens and the Geist type family. Every primitive is a thin wrapper over bits-ui, exposing a familiar set of variants.
Components render without internal wrappers — compose them inline, style them with semantic tokens, ship them as handoff references for devs.
Prototypes ship as routes under /prototypes/<slug>. Each route is
a single Svelte file that imports directly from $lib/components/ui/*.
No build step beyond SvelteKit's own — the repo is the handbook.
Feedback lands in BRIEF.md with a `DESIGN UPDATE NEEDED` tag and a screenshot. Code-wins-over-Figma cases are documented there too.
<script lang="ts">
import * as Accordion from '$lib/components/ui/accordion';
</script>
<Accordion.Root type="single">
<Accordion.Item value="a">
<Accordion.Trigger>Section one</Accordion.Trigger>
<Accordion.Content>Body copy…</Accordion.Content>
</Accordion.Item>
</Accordion.Root>Multiple open
Set type="multiple" to let several items stay open at once.
Opens independently of the others.
Stays open alongside its siblings.
Click to toggle this one too.
<script lang="ts" module>
type Box = { x: number; y: number; w: number; h: number } | null;
</script>
<script lang="ts">
import { Accordion as AccordionPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
value = $bindable(),
class: className,
children,
...restProps
}: AccordionPrimitive.RootProps = $props();
let hoverBox = $state<Box>(null);
let focusBox = $state<Box>(null);
function boxOf(el: HTMLElement, host: HTMLElement): Box {
const r = el.getBoundingClientRect();
const hr = host.getBoundingClientRect();
return { x: r.left - hr.left, y: r.top - hr.top, w: r.width, h: r.height };
}
function itemFor(target: EventTarget | null): HTMLElement | null {
return target instanceof Element
? target.closest<HTMLElement>('[data-slot="accordion-item"]')
: null;
}
function onPointerOver(event: PointerEvent) {
if (!ref) return;
const item = itemFor(event.target);
hoverBox = item ? boxOf(item, ref) : null;
}
function onPointerLeave() {
hoverBox = null;
}
function onFocusIn(event: FocusEvent) {
if (!ref) return;
const item = itemFor(event.target);
focusBox = item ? boxOf(item, ref) : null;
}
function onFocusOut(event: FocusEvent) {
if (!itemFor(event.relatedTarget)) focusBox = null;
}
function styleFor(box: Box): string {
if (!box) return 'opacity: 0;';
return `opacity: 1; transform: translate3d(${box.x}px, ${box.y}px, 0); width: ${box.w}px; height: ${box.h}px;`;
}
</script>
<AccordionPrimitive.Root bind:ref bind:value={value as never} {...restProps}>
{#snippet child({ props })}
<div
{...props}
data-slot="accordion"
class={cn(
'group/accordion relative flex w-full flex-col gap-0.5 overflow-hidden rounded-2xl border',
className
)}
onpointerover={onPointerOver}
onpointerleave={onPointerLeave}
onfocusin={onFocusIn}
onfocusout={onFocusOut}
>
<div
aria-hidden="true"
class="pointer-events-none absolute top-0 left-0 z-0 rounded-2xl bg-accent/40 transition-[transform,width,height,opacity] duration-(--motion-duration-fast) ease-(--motion-ease-out) will-change-transform dark:bg-accent/25"
style={styleFor(hoverBox)}
></div>
<div
aria-hidden="true"
class="pointer-events-none absolute top-0 left-0 z-0 rounded-2xl ring-(length:--ring-width) ring-ring/50 transition-[transform,width,height,opacity] duration-(--motion-duration-fast) ease-(--motion-ease-out) will-change-transform"
style={styleFor(focusBox)}
></div>
{@render children?.()}
</div>
{/snippet}
</AccordionPrimitive.Root>
Accordion.Root props
Inherits bits-ui Accordion.Root props. Item, Trigger, and Content pass through with minimal additions.
| Prop | Type | Default | Description |
|---|---|---|---|
'single' | 'multiple' | — | Single allows one item at a time; multiple lets several stay open. | |
string | string[] (bindable) | — | Controlled active item(s). String when type is single, array when multiple. | |
boolean | false | Disables every trigger in the group. | |
boolean | true | Roving focus wraps past the ends. | |
string | — | Merged onto the root container via tailwind-merge. |
Accordion.Trigger props
| Prop | Type | Default | Description |
|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 3 | Heading level applied to the internal AccordionPrimitive.Header. | |
string | — | Merged onto the trigger button. |