All components outlinesubtleenclosedsmmdlg
Accordion
Vertically stacked disclosure panels with outline, subtle, and enclosed variants.
StyleX extracts every rule to a static CSS file at build time — no runtime style injection.
1234567891011121314151617181920212223242526import { Accordion } from "#components/ui/base-ui/accordion.tsx";
export function Example() { return ( // size/variant resolve once on Root; every part inherits them via // context. Item captures the engine's open state through the render // prop, so the indicator rotates without extra wiring. <Accordion.Root defaultValue={["compile"]} variant="outline" size="md"> <Accordion.Item value="compile"> <Accordion.Header> <Accordion.Trigger> How are styles compiled? <Accordion.ItemIndicator> <ChevronIcon /> </Accordion.ItemIndicator> </Accordion.Trigger> </Accordion.Header> <Accordion.Panel> <Accordion.ItemBody> StyleX extracts every rule to static CSS at build time. </Accordion.ItemBody> </Accordion.Panel> </Accordion.Item> </Accordion.Root> );}Variants
StyleX extracts every rule to a static CSS file at build time — no runtime style injection.
StyleX extracts every rule to a static CSS file at build time — no runtime style injection.
StyleX extracts every rule to a static CSS file at build time — no runtime style injection.
Sizes
StyleX extracts every rule to a static CSS file at build time — no runtime style injection.
StyleX extracts every rule to a static CSS file at build time — no runtime style injection.
StyleX extracts every rule to a static CSS file at build time — no runtime style injection.
Install
1npx stylexblocks add accordionSource
components/ui/base-ui/accordion.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351import * as stylex from "@stylexjs/stylex";import { createContext, useContext } from "react";import { Accordion as BaseAccordion } from "@base-ui-components/react/accordion";import { createSlotRecipeContext } from "#lib/base-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, resolveSlotStyles, type StyleOverrideProps,} from "#lib/recipe-props.ts";import { accordionItemIndicatorIconVars } from '@stylexblocks/ui/slot-recipes/accordion.stylex.ts'
import { colorPalette } from '@stylexblocks/ui/color-palette.stylex.ts'import { durations } from '@stylexblocks/ui/tokens/durations.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { fontWeights } from '@stylexblocks/ui/tokens/font-weights.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticRadii } from '@stylexblocks/ui/semantic-tokens/radii.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'
const accordionRoot = stylex.create({ base: { width: sizes.full, '--accordion-radius': semanticRadii.panel, },})
const accordionRootSizes = stylex.create({ sm: { '--accordion-padding-x': spacing._3, '--accordion-padding-y': spacing._2, }, md: { '--accordion-padding-x': spacing._4, '--accordion-padding-y': spacing._2, }, lg: { '--accordion-padding-x': spacing._4_5, '--accordion-padding-y': spacing._2_5, },})
const accordionRootVariants = stylex.create({ enclosed: { borderWidth: '1px', borderRadius: 'var(--accordion-radius)', overflow: 'hidden', },})
const accordionItem = stylex.create({ base: { overflowAnchor: 'none', },})
const accordionItemVariants = stylex.create({ outline: { borderBottomWidth: '1px', }, subtle: { borderRadius: 'var(--accordion-radius)', }, enclosed: {},})
const accordionItemTrigger = stylex.create({ base: { display: 'flex', alignItems: 'center', width: sizes.full, outline: { default: 'none', ':focus-visible': '2px solid', }, gap: spacing._3, fontWeight: fontWeights.medium, borderRadius: 'var(--accordion-radius)', outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, cursor: { default: null, ':is(:disabled, [disabled], [data-disabled])': 'not-allowed', }, },})
const accordionItemTriggerSizes = stylex.create({ sm: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, paddingBlock: 'var(--accordion-padding-y)', }, md: { fontSize: fontSizes.md, lineHeight: lineHeights.md, paddingBlock: 'var(--accordion-padding-y)', }, lg: { fontSize: fontSizes.lg, lineHeight: lineHeights.lg, paddingBlock: 'var(--accordion-padding-y)', },})
const accordionItemTriggerVariants = stylex.create({ subtle: { paddingInline: 'var(--accordion-padding-x)', }, enclosed: { paddingInline: 'var(--accordion-padding-x)', },})
const accordionItemBody = stylex.create({ base: { paddingTop: 'var(--accordion-padding-y)', paddingBottom: 'calc(var(--accordion-padding-y) * 2)', },})
const accordionItemContent = stylex.create({ base: { overflow: 'hidden', borderRadius: 'var(--accordion-radius)', animationName: { default: null, ':is([data-open], [data-state="open"])': 'expand-height, fade-in', ':is([data-closed], [data-state="closed"])': 'collapse-height, fade-out', }, animationDuration: { default: null, ':is([data-open], [data-state="open"])': durations.moderate, ':is([data-closed], [data-state="closed"])': durations.moderate, }, },})
const accordionItemContentSizes = stylex.create({ sm: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, }, md: { fontSize: fontSizes.md, lineHeight: lineHeights.md, }, lg: { fontSize: fontSizes.lg, lineHeight: lineHeights.lg, },})
const accordionItemContentVariants = stylex.create({ subtle: { paddingInline: 'var(--accordion-padding-x)', }, enclosed: { paddingInline: 'var(--accordion-padding-x)', },})
const accordionItemIndicator = stylex.create({ base: { marginInlineStart: 'auto', transition: 'rotate 0.2s', transformOrigin: 'center', color: semanticColors.fgSubtle, [accordionItemIndicatorIconVars.width]: '1.2em', [accordionItemIndicatorIconVars.height]: '1.2em', },})
const accordionItemIndicatorIcon = stylex.create({ base: { width: accordionItemIndicatorIconVars.width, height: accordionItemIndicatorIconVars.height, },})
export type AccordionVariant = keyof typeof accordionRootVariants
export type AccordionSize = keyof typeof accordionRootSizes
// defineSlotRecipe validates the shape at compile time: default values must// exist in the axis maps, and slot keys must be `styles` or a declared axisconst accordionRecipe = defineSlotRecipe({ slots: { root: { styles: accordionRoot, sizes: accordionRootSizes, variants: accordionRootVariants, }, item: { styles: accordionItem, variants: accordionItemVariants }, itemTrigger: { styles: accordionItemTrigger, sizes: accordionItemTriggerSizes, variants: accordionItemTriggerVariants, }, itemBody: { styles: accordionItemBody }, itemContent: { styles: accordionItemContent, sizes: accordionItemContentSizes, variants: accordionItemContentVariants, }, itemIndicator: { styles: accordionItemIndicator }, }, defaultVariants: { size: "md", variant: "outline" },});
// Local chrome the recipe leaves to the composition layer: a native-button// reset under the trigger recipe, a heading reset for Base UI's h3 header// part, the indicator's open rotation, and the panel height bridge: the// shared expand/collapse-height keyframes animate to `var(--height)` (set// by Ark's machine), so Base UI's own measured var is aliased onto it.const chrome = stylex.create({ triggerReset: { backgroundColor: "transparent", borderStyle: "none", paddingInline: 0, paddingBlock: 0, cursor: "pointer", textAlign: "start", color: "inherit", fontFamily: "inherit", fontSize: "inherit", width: "100%", }, headerReset: { margin: 0, fontSize: "inherit", fontWeight: "inherit", }, indicator: { rotate: "0deg", }, indicatorOpen: { rotate: "180deg", }, panel: { "--height": "var(--accordion-panel-height)", },});
const { withProvider, withContext, VariantContext } = createSlotRecipeContext(accordionRecipe);
// Base UI has no indicator part; the Item captures the engine's open state// through the `render` prop and shares it so the indicator can rotate.const ItemOpenContext = createContext(false);
const ItemElement = BaseAccordion.Item as React.ElementType;
function Item( allProps: React.ComponentProps<typeof BaseAccordion.Item> & StyleOverrideProps,) { const { palette, style, className, ...rest } = allProps as Record<string, unknown>; const v = useContext(VariantContext); const sx = stylex.props( ...([ palette, ...resolveSlotStyles(accordionRecipe.slots.item, v), style, ] as unknown as ReadonlyArray<never>), ); return ( <ItemElement {...rest} className={ typeof className === "string" && className.length > 0 ? [sx.className, className].filter(Boolean).join(" ") : sx.className } style={sx.style} render={( props: React.ComponentProps<"div">, state: { open?: boolean }, ) => ( <ItemOpenContext.Provider value={Boolean(state.open)}> <div {...props} /> </ItemOpenContext.Provider> )} /> );}Item.displayName = "AccordionItem";
function ItemIndicator( allProps: React.ComponentProps<"span"> & StyleOverrideProps,) { const { palette, style, className, ...rest } = allProps as Record<string, unknown>; const v = useContext(VariantContext); const open = useContext(ItemOpenContext); const sx = stylex.props( ...([ palette, ...resolveSlotStyles(accordionRecipe.slots.itemIndicator, v), chrome.indicator, open && chrome.indicatorOpen, style, ] as unknown as ReadonlyArray<never>), ); return ( <span aria-hidden="true" data-state={open ? "open" : "closed"} {...(rest as React.ComponentProps<"span">)} className={ typeof className === "string" && className.length > 0 ? [sx.className, className].filter(Boolean).join(" ") : sx.className } style={sx.style} /> );}ItemIndicator.displayName = "AccordionItemIndicator";
/** * Accordion — Base UI's accordion machine wearing the accordion slot recipe: * keyboard navigation, single/multiple expansion, and panel state all come * from the engine (Item → Header → Trigger → Panel). `<Accordion.Root size * variant>` resolves the recipe once; every part inherits it through context * and composes via `render`. */export const Accordion = { Root: withProvider(BaseAccordion.Root, "root", "AccordionRoot"), Item, Header: withContext(BaseAccordion.Header, () => [chrome.headerReset], "AccordionHeader"), Trigger: withContext( BaseAccordion.Trigger, (v) => [chrome.triggerReset, ...resolveSlotStyles(accordionRecipe.slots.itemTrigger, v)], "AccordionTrigger", ), Panel: withContext( BaseAccordion.Panel, (v) => [...resolveSlotStyles(accordionRecipe.slots.itemContent, v), chrome.panel], "AccordionPanel", ), ItemBody: withContext("div", "itemBody", "AccordionItemBody"), ItemIndicator,};