TL;DR
@stacksjs/components's <Tabs> and <Accordion> define their content via a server-side string prop, not slot/children. That makes them unusable for any tab or accordion whose content is a component tree (which is essentially every real-world tab/accordion). After a sweep across four downstream apps (bench-review, paweldregan, trailbuddy, training), neither component is used anywhere — and the API friction is why.
What the API looks like today
packages/components/src/ui/tabs/Tabs.stx:
export interface Tab {
label: string
content: string // ← string only
icon?: string
}
export interface TabsProps {
tabs: Tab[]
...
}packages/components/src/ui/accordion/Accordion.stx:
export interface AccordionItem {
title: string
content: string // ← same
}
export interface AccordionProps {
items: AccordionItem[]
...
}The component bodies iterate tabs / items server-side via @foreach and interpolate as text. No <slot>, no per-item children.
Where this falls down
Every tab/accordion I audited in the training app contains component trees. A representative case from resources/views/settings.stx:
<div :if="activeSection === 'drivers'">
<DriversTable :drivers="drivers()" />
<ConnectDriverModal />
...
</div>
<div :if="activeSection === 'notifications'">
<NotificationsTable :rows="notifications()" />
...
</div>You can't express this with <Tabs tabs="[{label, content: ???}]" />. The team ended up with 40 manual :if="activeSection === '…'" switch-blocks across 4 files because the component doesn't fit. Same story for Accordion — 20 manual :show="openSections[…]" toggles in coach-sidebar.stx because the existing component can't hold an <EventCard> or <AthleteRow>.
Adoption signal
Across bench-review, paweldregan, trailbuddy, and training combined: zero <Tabs> usages, zero <Accordion> usages. The components ship, they're documented, but nobody can use them.
Proposed fix
Move to a slot-based API where each tab/accordion item is a child element with a label attribute:
<Tabs>
<TabPanel label="Drivers">
<DriversTable :drivers="drivers()" />
</TabPanel>
<TabPanel label="Notifications">
<NotificationsTable />
</TabPanel>
</Tabs>
<Accordion>
<AccordionItem title="Profile">
<ProfileEditor :user="user()" />
</AccordionItem>
<AccordionItem title="Zones">
<ZonesEditor />
</AccordionItem>
</Accordion>The container component still owns activeTab / openItems state and panel switching; the children just provide labels + arbitrary content.
This matches how Vue/React/Svelte tabs work, and (more importantly) matches how <Dialog> in the same package already works (slot-based with <DialogPanel> children).
Why this matters
The CLAUDE.md § "Reach for stx primitives before vanilla JS" guidance points agents at @stacksjs/components first. When the components can't actually express the common case, agents either (a) reinvent the pattern inline, or (b) churn through a refactor before discovering the API mismatch. Both happened in the latest audit.
🤖 Filed via Claude Code