+
+### AccordionGroup
+
+Container for managing multiple accordions. Renders a `
` element.
+
+
+
+## Examples
+
+### With Subtitle
+
+Here's a view when using both title and subtitle props.
+
+
}
+ code={accordionWithSubtitleSnippet}
+/>
+
+### Custom Trigger
+
+Here's a view when providing custom multi-line content as children.
+
+
}
+ code={accordionCustomTriggerSnippet}
+/>
+
+### Default Expanded
+
+Here's a view when the panel is expanded by default.
+
+
}
+ code={accordionDefaultExpandedSnippet}
+/>
+
+### Group Single Open
+
+Here's a view when only one accordion can be open at a time.
+
+
}
+ code={accordionGroupSingleOpenSnippet}
+/>
+
+### Group Multiple Open
+
+Here's a view when multiple accordions can be open simultaneously.
+
+
}
+ code={accordionGroupMultipleOpenSnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/content/accordion.props.ts b/docs-ui/src/content/accordion.props.ts
new file mode 100644
index 0000000000..aac983b0c3
--- /dev/null
+++ b/docs-ui/src/content/accordion.props.ts
@@ -0,0 +1,119 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const accordionPropDefs: Record
= {
+ children: {
+ type: 'enum',
+ values: ['ReactNode', '(state: { isExpanded: boolean }) => ReactNode'],
+ },
+ defaultExpanded: {
+ type: 'boolean',
+ default: 'false',
+ },
+ isExpanded: {
+ type: 'boolean',
+ },
+ onExpandedChange: {
+ type: 'enum',
+ values: ['(isExpanded: boolean) => void'],
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const accordionTriggerPropDefs: Record = {
+ level: {
+ type: 'enum',
+ values: ['1', '2', '3', '4', '5', '6'],
+ default: '3',
+ },
+ title: {
+ type: 'string',
+ },
+ subtitle: {
+ type: 'string',
+ },
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const accordionPanelPropDefs: Record = {
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const accordionGroupPropDefs: Record = {
+ allowsMultiple: {
+ type: 'boolean',
+ default: 'false',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const accordionUsageSnippet = `import { Accordion, AccordionTrigger, AccordionPanel } from '@backstage/ui';
+
+
+
+ Your content
+`;
+
+export const accordionWithSubtitleSnippet = `
+
+
+ Your content here
+
+`;
+
+export const accordionCustomTriggerSnippet = `
+
+
+ Custom Multi-line Trigger
+
+ Click to expand additional details
+
+
+
+
+ Your content here
+
+`;
+
+export const accordionDefaultExpandedSnippet = `
+
+
+ Your content here
+
+`;
+
+export const accordionGroupSingleOpenSnippet = `
+
+
+ Content 1
+
+
+
+ Content 2
+
+`;
+
+export const accordionGroupMultipleOpenSnippet = `
+
+
+ Content 1
+
+
+
+ Content 2
+
+`;
diff --git a/docs-ui/src/content/collapsible.mdx b/docs-ui/src/content/collapsible.mdx
deleted file mode 100644
index 948da323fe..0000000000
--- a/docs-ui/src/content/collapsible.mdx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { CodeBlock } from '@/components/CodeBlock';
-import { CollapsibleSnippet } from '@/snippets/stories-snippets';
-import {
- collapsibleRootPropDefs,
- collapsibleTriggerPropDefs,
- collapsiblePanelPropDefs,
- collapsibleUsageSnippet,
- collapsibleDefaultSnippet,
- collapsibleTriggerSnippet,
- collapsibleOpenSnippet,
-} from './collapsible.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
-}
- code={collapsibleDefaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-### Collapsible.Root
-
-Groups all parts of the collapsible. Renders a `` element.
-
-
-
-### Collapsible.Trigger
-
-The trigger by default render a simple unstyled button. Because menus can be rendered in different ways, we recommend
-using the `render` prop to render a custom trigger.
-
-
-
-
-
-### Collapsible.Panel
-
-A panel with the collapsible contents. Renders a `
` element.
-
-
-
-## Examples
-
-Open the panel by default by setting the `defaultOpen` prop to `true`.
-
-
}
- code={collapsibleOpenSnippet}
-/>
-
-
-
-
diff --git a/docs-ui/src/content/collapsible.props.ts b/docs-ui/src/content/collapsible.props.ts
deleted file mode 100644
index 55e80f6689..0000000000
--- a/docs-ui/src/content/collapsible.props.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- renderPropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const collapsibleRootPropDefs: Record
= {
- defaultOpen: {
- type: 'boolean',
- default: 'false',
- },
- open: {
- type: 'boolean',
- },
- onOpenChange: {
- type: 'enum',
- values: ['(open) => void'],
- },
- ...renderPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const collapsibleTriggerPropDefs: Record = {
- ...renderPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const collapsiblePanelPropDefs: Record = {
- hiddenUntilFound: {
- type: 'boolean',
- default: 'false',
- },
- keepMounted: {
- type: 'boolean',
- default: 'false',
- },
- ...renderPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const collapsibleUsageSnippet = `import { Collapsible } from '@backstage/ui';
-
-
- (
-
- )} />
- Your content
-`;
-
-export const collapsibleDefaultSnippet = `
- (
-
- )} />
-
-
- It's the edge of the world and all of Western civilization
- The sun may rise in the East, at least it settled in a final location
- It's understood that Hollywood sells Californication
-
-
-`;
-
-export const collapsibleTriggerSnippet = ` } />`;
-
-export const collapsibleOpenSnippet = `
- (
-
- )} />
-
-
- It's the edge of the world and all of Western civilization
- The sun may rise in the East, at least it settled in a final location
- It's understood that Hollywood sells Californication
-
-
-`;
diff --git a/docs-ui/src/snippets/stories-snippets.tsx b/docs-ui/src/snippets/stories-snippets.tsx
index 5e50c03558..0cb6119ce6 100644
--- a/docs-ui/src/snippets/stories-snippets.tsx
+++ b/docs-ui/src/snippets/stories-snippets.tsx
@@ -15,7 +15,7 @@ import * as SelectStories from '../../../packages/ui/src/components/Select/Selec
import * as MenuStories from '../../../packages/ui/src/components/Menu/Menu.stories';
import * as LinkStories from '../../../packages/ui/src/components/Link/Link.stories';
import * as AvatarStories from '../../../packages/ui/src/components/Avatar/Avatar.stories';
-import * as CollapsibleStories from '../../../packages/ui/src/components/Collapsible/Collapsible.stories';
+import * as AccordionStories from '../../../packages/ui/src/components/Accordion/Accordion.stories';
import * as DialogStories from '../../../packages/ui/src/components/Dialog/Dialog.stories';
import * as RadioGroupStories from '../../../packages/ui/src/components/RadioGroup/RadioGroup.stories';
import * as TabsStories from '../../../packages/ui/src/components/Tabs/Tabs.stories';
@@ -62,7 +62,7 @@ export const SelectSnippet = createSnippetComponent(SelectStories);
export const MenuSnippet = createSnippetComponent(MenuStories);
export const LinkSnippet = createSnippetComponent(LinkStories);
export const AvatarSnippet = createSnippetComponent(AvatarStories);
-export const CollapsibleSnippet = createSnippetComponent(CollapsibleStories);
+export const AccordionSnippet = createSnippetComponent(AccordionStories);
export const DialogSnippet = createSnippetComponent(DialogStories);
export const RadioGroupSnippet = createSnippetComponent(RadioGroupStories);
export const TabsSnippet = createSnippetComponent(TabsStories);
diff --git a/docs-ui/src/utils/changelog.ts b/docs-ui/src/utils/changelog.ts
index ef2e6c9fb9..05a06fdb9f 100644
--- a/docs-ui/src/utils/changelog.ts
+++ b/docs-ui/src/utils/changelog.ts
@@ -13,6 +13,7 @@ export type Component =
| 'datatable'
| 'select'
| 'collapsible'
+ | 'accordion'
| 'checkbox'
| 'container'
| 'link'
diff --git a/docs-ui/src/utils/data.ts b/docs-ui/src/utils/data.ts
index ae150b4c1e..de369043d1 100644
--- a/docs-ui/src/utils/data.ts
+++ b/docs-ui/src/utils/data.ts
@@ -24,6 +24,10 @@ export const layoutComponents: Page[] = [
];
export const components: Page[] = [
+ {
+ title: 'Accordion',
+ slug: 'accordion',
+ },
{
title: 'Avatar',
slug: 'avatar',
@@ -48,10 +52,6 @@ export const components: Page[] = [
title: 'Checkbox',
slug: 'checkbox',
},
- {
- title: 'Collapsible',
- slug: 'collapsible',
- },
{
title: 'Dialog',
slug: 'dialog',
diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md
index bfb0604a36..c1e1a309cc 100644
--- a/packages/ui/report.api.md
+++ b/packages/ui/report.api.md
@@ -6,12 +6,14 @@
import { ButtonProps as ButtonProps_2 } from 'react-aria-components';
import { CellProps as CellProps_2 } from 'react-aria-components';
import { CheckboxProps as CheckboxProps_2 } from 'react-aria-components';
-import { Collapsible as Collapsible_2 } from '@base-ui-components/react/collapsible';
import { ColumnProps as ColumnProps_2 } from 'react-aria-components';
import { ComponentProps } from 'react';
import type { ComponentPropsWithRef } from 'react';
import { DetailedHTMLProps } from 'react';
import type { DialogTriggerProps as DialogTriggerProps_2 } from 'react-aria-components';
+import type { DisclosureGroupProps } from 'react-aria-components';
+import type { DisclosurePanelProps } from 'react-aria-components';
+import type { DisclosureProps } from 'react-aria-components';
import type { ElementType } from 'react';
import { ForwardRefExoticComponent } from 'react';
import type { HeadingProps } from 'react-aria-components';
@@ -51,6 +53,57 @@ import type { TextFieldProps as TextFieldProps_2 } from 'react-aria-components';
import { TooltipProps as TooltipProps_2 } from 'react-aria-components';
import { TooltipTriggerComponentProps } from 'react-aria-components';
+// @public (undocumented)
+export const Accordion: ForwardRefExoticComponent<
+ AccordionProps & RefAttributes
+>;
+
+// @public (undocumented)
+export const AccordionGroup: ForwardRefExoticComponent<
+ AccordionGroupProps & RefAttributes
+>;
+
+// @public
+export interface AccordionGroupProps extends DisclosureGroupProps {
+ allowsMultiple?: boolean;
+ // (undocumented)
+ className?: string;
+}
+
+// @public (undocumented)
+export const AccordionPanel: ForwardRefExoticComponent<
+ AccordionPanelProps & RefAttributes
+>;
+
+// @public
+export interface AccordionPanelProps extends DisclosurePanelProps {
+ // (undocumented)
+ className?: string;
+}
+
+// @public
+export interface AccordionProps extends DisclosureProps {
+ // (undocumented)
+ className?: string;
+}
+
+// @public (undocumented)
+export const AccordionTrigger: ForwardRefExoticComponent<
+ AccordionTriggerProps & RefAttributes
+>;
+
+// @public
+export interface AccordionTriggerProps extends HeadingProps {
+ // (undocumented)
+ children?: React.ReactNode;
+ // (undocumented)
+ className?: string;
+ // (undocumented)
+ subtitle?: string;
+ // (undocumented)
+ title?: string;
+}
+
// @public (undocumented)
export type AlignItems = 'stretch' | 'start' | 'center' | 'end';
@@ -279,25 +332,6 @@ export interface CheckboxProps extends CheckboxProps_2 {
// @public
export type ClassNamesMap = Record;
-// @public
-export const Collapsible: {
- Root: ForwardRefExoticComponent<
- Omit, 'ref'> &
- RefAttributes
- >;
- Trigger: ForwardRefExoticComponent<
- Omit<
- Collapsible_2.Trigger.Props & RefAttributes,
- 'ref'
- > &
- RefAttributes
- >;
- Panel: ForwardRefExoticComponent<
- Omit, 'ref'> &
- RefAttributes
- >;
-};
-
// @public (undocumented)
export const Column: (props: ColumnProps) => JSX_2.Element;
@@ -417,13 +451,6 @@ export const componentDefinitions: {
readonly selected: readonly [true, false];
};
};
- readonly Collapsible: {
- readonly classNames: {
- readonly root: 'bui-CollapsibleRoot';
- readonly trigger: 'bui-CollapsibleTrigger';
- readonly panel: 'bui-CollapsiblePanel';
- };
- };
readonly Container: {
readonly classNames: {
readonly root: 'bui-Container';
@@ -440,6 +467,18 @@ export const componentDefinitions: {
readonly footer: 'bui-DialogFooter';
};
};
+ readonly Accordion: {
+ readonly classNames: {
+ readonly root: 'bui-Accordion';
+ readonly trigger: 'bui-AccordionTrigger';
+ readonly triggerButton: 'bui-AccordionTriggerButton';
+ readonly triggerTitle: 'bui-AccordionTriggerTitle';
+ readonly triggerSubtitle: 'bui-AccordionTriggerSubtitle';
+ readonly triggerIcon: 'bui-AccordionTriggerIcon';
+ readonly panel: 'bui-AccordionPanel';
+ readonly group: 'bui-AccordionGroup';
+ };
+ };
readonly FieldError: {
readonly classNames: {
readonly root: 'bui-FieldError';
diff --git a/packages/ui/src/components/Accordion/Accordion.module.css b/packages/ui/src/components/Accordion/Accordion.module.css
new file mode 100644
index 0000000000..a86400986d
--- /dev/null
+++ b/packages/ui/src/components/Accordion/Accordion.module.css
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@layer tokens, base, components, utilities;
+
+@layer components {
+ .bui-Accordion {
+ width: 100%;
+ background-color: var(--bui-bg-surface-1);
+ border-radius: var(--bui-radius-3);
+ padding: var(--bui-space-3);
+ }
+
+ .bui-AccordionTrigger {
+ all: unset;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .bui-AccordionTriggerButton {
+ all: unset;
+ width: 100%;
+ color: var(--bui-fg-primary);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ cursor: pointer;
+ text-align: left;
+
+ &:focus-visible {
+ outline: none;
+ transition: none;
+ box-shadow: inset 0 0 0 2px var(--bui-ring);
+ }
+
+ &[data-disabled='true'] {
+ background-color: transparent;
+ color: var(--bui-fg-disabled);
+ cursor: not-allowed;
+ }
+ }
+
+ .bui-AccordionTriggerTitle {
+ font-size: var(--bui-font-size-4);
+ font-weight: var(--bui-font-weight-bold);
+ line-height: 140%;
+ }
+
+ .bui-AccordionTriggerSubtitle {
+ font-size: var(--bui-font-size-2);
+ line-height: 140%;
+ color: var(--bui-fg-secondary);
+ }
+
+ .bui-AccordionTriggerIcon {
+ transition: transform 150ms ease-out;
+ flex-shrink: 0;
+ width: 1rem;
+ height: 1rem;
+
+ [data-expanded='true'] & {
+ transform: rotate(180deg);
+ }
+ }
+
+ .bui-AccordionPanel {
+ [data-expanded='true'] & {
+ padding-top: var(--bui-space-1);
+ }
+ }
+
+ .bui-AccordionGroup {
+ display: flex;
+ flex-direction: column;
+ gap: var(--bui-space-3);
+ width: 100%;
+ }
+}
diff --git a/packages/ui/src/components/Accordion/Accordion.stories.tsx b/packages/ui/src/components/Accordion/Accordion.stories.tsx
new file mode 100644
index 0000000000..6640bd5d6a
--- /dev/null
+++ b/packages/ui/src/components/Accordion/Accordion.stories.tsx
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import {
+ Accordion,
+ AccordionTrigger,
+ AccordionPanel,
+ AccordionGroup,
+} from './Accordion';
+import { Box } from '../Box';
+import { Text } from '../Text';
+
+const Content = () => (
+
+
+ It's the edge of the world and all of Western civilization
+
+
+ The sun may rise in the East, at least it settled in a final location
+
+ It's understood that Hollywood sells Californication
+
+);
+
+const meta = {
+ title: 'Backstage UI/Accordion',
+ component: Accordion,
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ render: () => (
+
+
+
+
+
+
+ ),
+};
+
+export const WithSubtitle: Story = {
+ render: () => (
+
+
+
+
+
+
+ ),
+};
+
+export const CustomTrigger: Story = {
+ render: () => (
+
+
+
+
+ Custom Multi-line Trigger
+
+
+ Click to expand additional details and configuration options
+
+
+
+
+
+
+
+ ),
+};
+
+export const DefaultExpanded: Story = {
+ render: () => (
+
+
+
+
+
+
+ ),
+};
+
+export const GroupSingleOpen: Story = {
+ render: () => (
+
+
+
+
+
+
+ It's the edge of the world and all of Western civilization
+
+
+
+
+
+
+
+
+
+ The sun may rise in the East, at least it settled in a final
+ location
+
+
+
+
+
+
+
+
+
+ It's understood that Hollywood sells Californication
+
+
+
+
+
+ ),
+};
+
+export const GroupMultipleOpen: Story = {
+ render: () => (
+
+
+
+
+
+
+ It's the edge of the world and all of Western civilization
+
+
+
+
+
+
+
+
+
+ The sun may rise in the East, at least it settled in a final
+ location
+
+
+
+
+
+
+
+
+
+ It's understood that Hollywood sells Californication
+
+
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Accordion/Accordion.tsx b/packages/ui/src/components/Accordion/Accordion.tsx
new file mode 100644
index 0000000000..3f35ab7554
--- /dev/null
+++ b/packages/ui/src/components/Accordion/Accordion.tsx
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { forwardRef } from 'react';
+import {
+ Disclosure as RADisclosure,
+ Button as RAButton,
+ DisclosurePanel as RADisclosurePanel,
+ DisclosureGroup as RADisclosureGroup,
+ Heading as RAHeading,
+} from 'react-aria-components';
+import { RiArrowDownSLine } from '@remixicon/react';
+import clsx from 'clsx';
+import type {
+ AccordionProps,
+ AccordionTriggerProps,
+ AccordionPanelProps,
+ AccordionGroupProps,
+} from './types';
+import { useStyles } from '../../hooks/useStyles';
+import styles from './Accordion.module.css';
+import { Flex } from '../Flex';
+
+/** @public */
+export const Accordion = forwardRef<
+ React.ElementRef,
+ AccordionProps
+>(({ className, ...props }, ref) => {
+ const { classNames, cleanedProps } = useStyles('Accordion', props);
+
+ return (
+
+ );
+});
+
+Accordion.displayName = 'Accordion';
+
+/** @public */
+export const AccordionTrigger = forwardRef<
+ React.ElementRef,
+ AccordionTriggerProps
+>(({ className, title, subtitle, children, ...props }, ref) => {
+ const { classNames, cleanedProps } = useStyles('Accordion', props);
+
+ return (
+
+
+ {children ? (
+ children
+ ) : (
+
+
+ {title}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+ )}
+
+
+
+
+ );
+});
+
+AccordionTrigger.displayName = 'AccordionTrigger';
+
+/** @public */
+export const AccordionPanel = forwardRef<
+ React.ElementRef,
+ AccordionPanelProps
+>(({ className, ...props }, ref) => {
+ const { classNames, cleanedProps } = useStyles('Accordion', props);
+
+ return (
+
+ );
+});
+
+AccordionPanel.displayName = 'AccordionPanel';
+
+/** @public */
+export const AccordionGroup = forwardRef<
+ React.ElementRef,
+ AccordionGroupProps
+>(({ className, allowsMultiple = false, ...props }, ref) => {
+ const { classNames, cleanedProps } = useStyles('Accordion', props);
+
+ return (
+
+ );
+});
+
+AccordionGroup.displayName = 'AccordionGroup';
diff --git a/packages/ui/src/components/Collapsible/index.ts b/packages/ui/src/components/Accordion/index.ts
similarity index 73%
rename from packages/ui/src/components/Collapsible/index.ts
rename to packages/ui/src/components/Accordion/index.ts
index 6a7f8e8816..cd34b4bc87 100644
--- a/packages/ui/src/components/Collapsible/index.ts
+++ b/packages/ui/src/components/Accordion/index.ts
@@ -14,4 +14,15 @@
* limitations under the License.
*/
-export { Collapsible } from './Collapsible';
+export {
+ Accordion,
+ AccordionTrigger,
+ AccordionPanel,
+ AccordionGroup,
+} from './Accordion';
+export type {
+ AccordionProps,
+ AccordionTriggerProps,
+ AccordionPanelProps,
+ AccordionGroupProps,
+} from './types';
diff --git a/packages/ui/src/components/Accordion/types.ts b/packages/ui/src/components/Accordion/types.ts
new file mode 100644
index 0000000000..06c8549ebd
--- /dev/null
+++ b/packages/ui/src/components/Accordion/types.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type {
+ DisclosureProps as RADisclosureProps,
+ HeadingProps as RAHeadingProps,
+ DisclosurePanelProps as RADisclosurePanelProps,
+ DisclosureGroupProps as RADisclosureGroupProps,
+} from 'react-aria-components';
+
+/**
+ * Props for the Accordion component.
+ * @public
+ */
+export interface AccordionProps extends RADisclosureProps {
+ className?: string;
+}
+
+/**
+ * Props for the AccordionTrigger component.
+ * @public
+ */
+export interface AccordionTriggerProps extends RAHeadingProps {
+ className?: string;
+ title?: string;
+ subtitle?: string;
+ children?: React.ReactNode;
+}
+
+/**
+ * Props for the AccordionPanel component.
+ * @public
+ */
+export interface AccordionPanelProps extends RADisclosurePanelProps {
+ className?: string;
+}
+
+/**
+ * Props for the AccordionGroup component.
+ * @public
+ */
+export interface AccordionGroupProps extends RADisclosureGroupProps {
+ className?: string;
+ /**
+ * Whether multiple accordions can be expanded at the same time.
+ * @defaultValue false
+ */
+ allowsMultiple?: boolean;
+}
diff --git a/packages/ui/src/components/Collapsible/Collapsible.module.css b/packages/ui/src/components/Collapsible/Collapsible.module.css
deleted file mode 100644
index d3b394011c..0000000000
--- a/packages/ui/src/components/Collapsible/Collapsible.module.css
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright 2025 The Backstage Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@layer tokens, base, components, utilities;
-
-@layer components {
- .bui-CollapsiblePanel {
- display: flex;
- height: var(--collapsible-panel-height);
- overflow: hidden;
- transition: all 150ms ease-out;
-
- &[data-starting-style],
- &[data-ending-style] {
- height: 0;
- }
- }
-}
diff --git a/packages/ui/src/components/Collapsible/Collapsible.stories.tsx b/packages/ui/src/components/Collapsible/Collapsible.stories.tsx
deleted file mode 100644
index 47b76f2f56..0000000000
--- a/packages/ui/src/components/Collapsible/Collapsible.stories.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright 2025 The Backstage Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import type { Meta, StoryObj } from '@storybook/react-vite';
-import { Collapsible } from './Collapsible';
-import { Button } from '../Button';
-import { Box } from '../Box';
-import { Text } from '../Text';
-import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react';
-
-const meta = {
- title: 'Backstage UI/Collapsible',
- component: Collapsible.Root,
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {
- args: {
- style: {
- display: 'flex',
- flexDirection: 'column',
- gap: 'var(--bui-space-2)',
- alignItems: 'center',
- },
- children: (
- <>
- (
- : }
- {...props}
- >
- {state.open ? 'Close Panel' : 'Open Panel'}
-
- )}
- />
-
-
-
- It's the edge of the world and all of Western civilization
-
-
- The sun may rise in the East, at least it settled in a final
- location
-
- It's understood that Hollywood sells Californication
-
-
- >
- ),
- },
-};
-
-export const Open: Story = {
- args: {
- ...Default.args,
- defaultOpen: true,
- },
-};
diff --git a/packages/ui/src/components/Collapsible/Collapsible.tsx b/packages/ui/src/components/Collapsible/Collapsible.tsx
deleted file mode 100644
index c9417d2019..0000000000
--- a/packages/ui/src/components/Collapsible/Collapsible.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright 2025 The Backstage Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { forwardRef } from 'react';
-import { Collapsible as CollapsiblePrimitive } from '@base-ui-components/react/collapsible';
-import clsx from 'clsx';
-import { useStyles } from '../../hooks/useStyles';
-import styles from './Collapsible.module.css';
-
-const CollapsibleRoot = forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => {
- const { classNames, cleanedProps } = useStyles('Collapsible', props);
-
- return (
-
- );
-});
-CollapsibleRoot.displayName = CollapsiblePrimitive.Root.displayName;
-
-const CollapsibleTrigger = forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => {
- const { classNames, cleanedProps } = useStyles('Collapsible', props);
-
- return (
-
- );
-});
-CollapsibleTrigger.displayName = CollapsiblePrimitive.Trigger.displayName;
-
-const CollapsiblePanel = forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => {
- const { classNames, cleanedProps } = useStyles('Collapsible', props);
-
- return (
-
- );
-});
-CollapsiblePanel.displayName = CollapsiblePrimitive.Panel.displayName;
-
-/**
- * Collapsible is a component that allows you to collapse and expand content.
- * It is a wrapper around the CollapsiblePrimitive component from base-ui-components.
- *
- * @public
- */
-export const Collapsible = {
- Root: CollapsibleRoot,
- Trigger: CollapsibleTrigger,
- Panel: CollapsiblePanel,
-};
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 1bb10cfbd1..baa4c3e5dc 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -30,7 +30,7 @@ export * from './components/Container';
export * from './components/Avatar';
export * from './components/Button';
export * from './components/Card';
-export * from './components/Collapsible';
+export * from './components/Accordion';
export * from './components/Dialog';
export * from './components/FieldLabel';
export * from './components/Header';
diff --git a/packages/ui/src/utils/componentDefinitions.ts b/packages/ui/src/utils/componentDefinitions.ts
index 8f44e1e730..9b18a6feaf 100644
--- a/packages/ui/src/utils/componentDefinitions.ts
+++ b/packages/ui/src/utils/componentDefinitions.ts
@@ -96,13 +96,6 @@ export const componentDefinitions = {
selected: [true, false] as const,
},
},
- Collapsible: {
- classNames: {
- root: 'bui-CollapsibleRoot',
- trigger: 'bui-CollapsibleTrigger',
- panel: 'bui-CollapsiblePanel',
- },
- },
Container: {
classNames: {
root: 'bui-Container',
@@ -119,6 +112,18 @@ export const componentDefinitions = {
footer: 'bui-DialogFooter',
},
},
+ Accordion: {
+ classNames: {
+ root: 'bui-Accordion',
+ trigger: 'bui-AccordionTrigger',
+ triggerButton: 'bui-AccordionTriggerButton',
+ triggerTitle: 'bui-AccordionTriggerTitle',
+ triggerSubtitle: 'bui-AccordionTriggerSubtitle',
+ triggerIcon: 'bui-AccordionTriggerIcon',
+ panel: 'bui-AccordionPanel',
+ group: 'bui-AccordionGroup',
+ },
+ },
FieldError: {
classNames: {
root: 'bui-FieldError',