diff --git a/apps/www/src/components/typetable/typetable.tsx b/apps/www/src/components/typetable/typetable.tsx
index 2f15a29ff..e4201045d 100644
--- a/apps/www/src/components/typetable/typetable.tsx
+++ b/apps/www/src/components/typetable/typetable.tsx
@@ -57,7 +57,7 @@ export function TypeTable({
Prop
Type
-
+
{entries.map(([key, value]) => (
))}
diff --git a/apps/www/src/content/docs/components/accordion/demo.ts b/apps/www/src/content/docs/components/accordion/demo.ts
index f4725decc..6bfb7104e 100644
--- a/apps/www/src/content/docs/components/accordion/demo.ts
+++ b/apps/www/src/content/docs/components/accordion/demo.ts
@@ -28,10 +28,9 @@ export const getCode = (props: Record) => {
export const playground = {
type: 'playground',
controls: {
- type: {
- type: 'select',
- options: ['single', 'multiple'],
- defaultValue: 'single'
+ multiple: {
+ type: 'checkbox',
+ defaultValue: false
}
},
getCode
@@ -43,7 +42,7 @@ export const typeDemo = {
{
name: 'Single',
code: `
-
+
What is Apsara?
@@ -67,7 +66,7 @@ export const typeDemo = {
{
name: 'Multiple',
code: `
-
+
What is Apsara?
diff --git a/apps/www/src/content/docs/components/accordion/index.mdx b/apps/www/src/content/docs/components/accordion/index.mdx
index 4945a75b7..5f90d1302 100644
--- a/apps/www/src/content/docs/components/accordion/index.mdx
+++ b/apps/www/src/content/docs/components/accordion/index.mdx
@@ -40,10 +40,10 @@ import { Accordion } from '@raystack/apsara'
### Single vs Multiple
-The Accordion component supports two types of behavior:
+The Accordion component supports two modes:
-- **Single**: Only one item can be open at a time
-- **Multiple**: Multiple items can be open simultaneously
+- **Single** (default): Only one item can be open at a time. Set `multiple={false}` or omit the prop.
+- **Multiple**: Multiple items can be open simultaneously. Set `multiple={true}`.
@@ -64,20 +64,3 @@ Individual accordion items can be disabled using the `disabled` prop.
The accordion content can contain any React elements, allowing for rich layouts and complex content.
-
-## Accessibility
-
-The Accordion component is built on top of [Radix UI's Accordion primitive](https://www.radix-ui.com/primitives/docs/components/accordion) and follows the WAI-ARIA design pattern for accordions. It includes:
-
-- Proper ARIA attributes for screen readers
-- Keyboard navigation support
-- Focus management
-- Semantic HTML structure
-
-## Keyboard Navigation
-
-- **Space** or **Enter**: Toggle the focused accordion item
-- **Arrow Down**: Move focus to the next accordion item
-- **Arrow Up**: Move focus to the previous accordion item
-- **Home**: Move focus to the first accordion item
-- **End**: Move focus to the last accordion item
diff --git a/apps/www/src/content/docs/components/accordion/props.ts b/apps/www/src/content/docs/components/accordion/props.ts
index a520ea8d6..cd71eeedb 100644
--- a/apps/www/src/content/docs/components/accordion/props.ts
+++ b/apps/www/src/content/docs/components/accordion/props.ts
@@ -1,32 +1,30 @@
export interface AccordionRootProps {
/**
- * Controls how many accordion items can be open at once.
- * - "single": Only one item can be open at a time
- * - "multiple": Multiple items can be open simultaneously
- * @defaultValue "single"
+ * Whether multiple accordion items can be open at the same time.
+ * @defaultValue false
*/
- type?: 'single' | 'multiple';
+ multiple?: boolean;
/**
- * The controlled value of the accordion
+ * The controlled value of the accordion.
+ * For single mode: string | undefined
+ * For multiple mode: string[]
*/
value?: string | string[];
/**
- * The default value of the accordion
+ * The default value of the accordion.
+ * For single mode: string | undefined
+ * For multiple mode: string[]
*/
defaultValue?: string | string[];
/**
- * Event handler called when the value changes
- */
- onValueChange?: (value: string | string[]) => void;
-
- /**
- * Whether the accordion is collapsible when type is single
- * @defaultValue true
+ * Event handler called when the value changes.
+ * For single mode: (value?: string) => void
+ * For multiple mode: (value?: string[]) => void
*/
- collapsible?: boolean;
+ onValueChange?: (value?: string | string[]) => void;
/**
* Whether the accordion is disabled
@@ -41,10 +39,22 @@ export interface AccordionRootProps {
orientation?: 'horizontal' | 'vertical';
/**
- * The direction of the accordion
- * @defaultValue "ltr"
+ * Whether to loop keyboard focus back to the first item when the end is reached
+ * @defaultValue true
+ */
+ loopFocus?: boolean;
+
+ /**
+ * Whether to keep the element in the DOM while the panel is closed
+ * @defaultValue false
+ */
+ keepMounted?: boolean;
+
+ /**
+ * Allows the browser's built-in page search to find and expand the panel contents
+ * @defaultValue false
*/
- dir?: 'ltr' | 'rtl';
+ hiddenUntilFound?: boolean;
/** Custom CSS class names */
className?: string;
@@ -73,10 +83,16 @@ export interface AccordionTriggerProps {
export interface AccordionContentProps {
/**
- * Whether the content is force mounted
+ * Whether to keep the element in the DOM while the panel is closed
+ * @defaultValue false
+ */
+ keepMounted?: boolean;
+
+ /**
+ * Allows the browser's built-in page search to find and expand the panel contents
* @defaultValue false
*/
- forceMount?: boolean;
+ hiddenUntilFound?: boolean;
/** Custom CSS class names */
className?: string;
diff --git a/apps/www/src/content/docs/components/slider/index.mdx b/apps/www/src/content/docs/components/slider/index.mdx
index 938bbe460..7da30ed42 100644
--- a/apps/www/src/content/docs/components/slider/index.mdx
+++ b/apps/www/src/content/docs/components/slider/index.mdx
@@ -11,7 +11,7 @@ import { playground, variantDemo, controlDemo, thumbSizeDemo } from "./demo.ts";
## Usage
```tsx
-import { Slider } from '@raystack/apsara'
+import { Slider, SliderValue } from '@raystack/apsara'
```
## Slider Props
diff --git a/packages/raystack/components/accordion/__tests__/accordion.test.tsx b/packages/raystack/components/accordion/__tests__/accordion.test.tsx
index d1bbaa8ac..061927350 100644
--- a/packages/raystack/components/accordion/__tests__/accordion.test.tsx
+++ b/packages/raystack/components/accordion/__tests__/accordion.test.tsx
@@ -2,8 +2,8 @@ import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import { Accordion } from '../accordion';
-import { AccordionRootProps } from '../accordion-root';
import styles from '../accordion.module.css';
+import { AccordionRootProps } from '../accordion-root';
const ITEM_1_TEXT = 'Item 1';
const ITEM_2_TEXT = 'Item 2';
@@ -183,7 +183,8 @@ describe('Accordion', () => {
render();
const trigger = screen.getByRole('button', { name: ITEM_2_TEXT });
- expect(trigger).toBeDisabled();
+ // Base UI uses aria-disabled instead of disabled attribute
+ expect(trigger).toHaveAttribute('aria-disabled', 'true');
});
});
@@ -207,20 +208,21 @@ describe('Accordion', () => {
it('forwards ref correctly', () => {
const ref = vi.fn();
render(
-
+
{ITEM_1_TEXT}
{CONTENT_1_TEXT}
);
+ // Base UI Panel ref should be called when panel is open
expect(ref).toHaveBeenCalled();
});
});
describe('Multiple Items', () => {
it('handles multiple accordion items independently', () => {
- render();
+ render();
const trigger1 = screen.getByRole('button', { name: ITEM_1_TEXT });
const trigger2 = screen.getByRole('button', { name: ITEM_2_TEXT });
diff --git a/packages/raystack/components/accordion/accordion-content.tsx b/packages/raystack/components/accordion/accordion-content.tsx
index 794697ffa..5dc7183bc 100644
--- a/packages/raystack/components/accordion/accordion-content.tsx
+++ b/packages/raystack/components/accordion/accordion-content.tsx
@@ -1,21 +1,15 @@
'use client';
+import { Accordion as AccordionPrimitive } from '@base-ui/react';
import { cx } from 'class-variance-authority';
-import { Accordion as AccordionPrimitive } from 'radix-ui';
-import { ElementRef, ReactNode, forwardRef } from 'react';
+import { ElementRef, forwardRef } from 'react';
import styles from './accordion.module.css';
-export interface AccordionContentProps
- extends AccordionPrimitive.AccordionContentProps {
- children: ReactNode;
- className?: string;
-}
-
export const AccordionContent = forwardRef<
- ElementRef,
- AccordionContentProps
+ ElementRef,
+ AccordionPrimitive.Panel.Props
>(({ className, children, ...props }, ref) => (
-
{children}
-
+
));
-AccordionContent.displayName = AccordionPrimitive.Content.displayName;
+AccordionContent.displayName = 'Accordion.Content';
diff --git a/packages/raystack/components/accordion/accordion-item.tsx b/packages/raystack/components/accordion/accordion-item.tsx
index 58af12e68..3b487fa5c 100644
--- a/packages/raystack/components/accordion/accordion-item.tsx
+++ b/packages/raystack/components/accordion/accordion-item.tsx
@@ -1,19 +1,13 @@
'use client';
+import { Accordion as AccordionPrimitive } from '@base-ui/react';
import { cx } from 'class-variance-authority';
-import { Accordion as AccordionPrimitive } from 'radix-ui';
-import { ElementRef, ReactNode, forwardRef } from 'react';
+import { ElementRef, forwardRef } from 'react';
import styles from './accordion.module.css';
-export interface AccordionItemProps
- extends AccordionPrimitive.AccordionItemProps {
- children: ReactNode;
- className?: string;
-}
-
export const AccordionItem = forwardRef<
ElementRef,
- AccordionItemProps
+ AccordionPrimitive.Item.Props
>(({ className, children, ...props }, ref) => (
));
-AccordionItem.displayName = AccordionPrimitive.Item.displayName;
+AccordionItem.displayName = 'Accordion.Item';
diff --git a/packages/raystack/components/accordion/accordion-root.tsx b/packages/raystack/components/accordion/accordion-root.tsx
index 75d5bfbcc..be7030010 100644
--- a/packages/raystack/components/accordion/accordion-root.tsx
+++ b/packages/raystack/components/accordion/accordion-root.tsx
@@ -1,46 +1,87 @@
'use client';
+import { Accordion as AccordionPrimitive } from '@base-ui/react';
import { cx } from 'class-variance-authority';
-import { Accordion as AccordionPrimitive } from 'radix-ui';
import { ElementRef, forwardRef } from 'react';
import styles from './accordion.module.css';
type AccordionSingleProps = Omit<
- AccordionPrimitive.AccordionSingleProps,
- 'type'
+ AccordionPrimitive.Root.Props,
+ 'multiple' | 'value' | 'defaultValue' | 'onValueChange'
> & {
- type?: 'single';
+ multiple?: false;
+ value?: string;
+ defaultValue?: string;
+ onValueChange?: (value?: string) => void;
};
+
type AccordionMultipleProps = Omit<
- AccordionPrimitive.AccordionMultipleProps,
- 'type'
+ AccordionPrimitive.Root.Props,
+ 'multiple' | 'value' | 'defaultValue' | 'onValueChange'
> & {
- type: 'multiple';
+ multiple: true;
+ value?: string[];
+ defaultValue?: string[];
+ onValueChange?: (value?: string[]) => void;
};
+
export type AccordionRootProps = AccordionSingleProps | AccordionMultipleProps;
export const AccordionRoot = forwardRef<
ElementRef,
AccordionRootProps
->(({ className, type = 'single', ...rest }, ref) => {
- // this is a workaround to properly typecast the union type
- const singleProps = {
- type: 'single',
- collapsible: true,
- ...rest
- } as AccordionPrimitive.AccordionSingleProps;
- const multipleProps = {
- type: 'multiple',
- ...rest
- } as AccordionPrimitive.AccordionMultipleProps;
-
- return (
-
- );
-});
-
-AccordionRoot.displayName = AccordionPrimitive.Root.displayName;
+>(
+ (
+ {
+ className,
+ multiple = false,
+ value,
+ defaultValue,
+ onValueChange,
+ ...rest
+ },
+ ref
+ ) => {
+ // Convert value to array format for Base UI
+ const baseValue = value
+ ? Array.isArray(value)
+ ? value
+ : [value]
+ : undefined;
+
+ const baseDefaultValue = defaultValue
+ ? Array.isArray(defaultValue)
+ ? defaultValue
+ : [defaultValue]
+ : undefined;
+
+ const handleValueChange = (
+ newValue: string[],
+ eventDetails: AccordionPrimitive.Root.ChangeEventDetails
+ ) => {
+ if (onValueChange) {
+ if (multiple) {
+ (onValueChange as (value: string[]) => void)(newValue);
+ } else {
+ (onValueChange as (value: string | undefined) => void)(
+ newValue[0] || undefined
+ );
+ }
+ }
+ };
+
+ return (
+
+ );
+ }
+);
+
+AccordionRoot.displayName = 'Accordion.Root';
diff --git a/packages/raystack/components/accordion/accordion-trigger.tsx b/packages/raystack/components/accordion/accordion-trigger.tsx
index 997fff59c..ef50a0ad3 100644
--- a/packages/raystack/components/accordion/accordion-trigger.tsx
+++ b/packages/raystack/components/accordion/accordion-trigger.tsx
@@ -1,20 +1,14 @@
'use client';
+import { Accordion as AccordionPrimitive } from '@base-ui/react';
import { ChevronDownIcon } from '@radix-ui/react-icons';
import { cx } from 'class-variance-authority';
-import { Accordion as AccordionPrimitive } from 'radix-ui';
-import { ElementRef, ReactNode, forwardRef } from 'react';
+import { ElementRef, forwardRef } from 'react';
import styles from './accordion.module.css';
-export interface AccordionTriggerProps
- extends AccordionPrimitive.AccordionTriggerProps {
- children: ReactNode;
- className?: string;
-}
-
export const AccordionTrigger = forwardRef<
ElementRef,
- AccordionTriggerProps
+ AccordionPrimitive.Trigger.Props
>(({ className, children, ...props }, ref) => (
));
-AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
+AccordionTrigger.displayName = 'Accordion.Trigger';
diff --git a/packages/raystack/components/accordion/accordion.module.css b/packages/raystack/components/accordion/accordion.module.css
index 28eb4b7c5..a8db803e7 100644
--- a/packages/raystack/components/accordion/accordion.module.css
+++ b/packages/raystack/components/accordion/accordion.module.css
@@ -39,7 +39,7 @@
opacity: 0.5;
}
-.accordion-trigger[data-state="open"] .accordion-icon {
+.accordion-trigger[data-panel-open] .accordion-icon {
transform: rotate(180deg);
}
@@ -53,7 +53,14 @@
}
.accordion-content {
+ height: var(--accordion-panel-height);
overflow: hidden;
+ transition: height 150ms ease-out;
+}
+
+.accordion-content[data-starting-style],
+.accordion-content[data-ending-style] {
+ height: 0;
}
.accordion-content-inner {
@@ -70,30 +77,3 @@
padding: var(--rs-space-5) var(--rs-space-4);
border-top: 0px;
}
-
-.accordion-content[data-state="closed"] {
- animation: accordion-up 200ms ease-out;
-}
-
-.accordion-content[data-state="open"] {
- animation: accordion-down 200ms ease-out;
-}
-
-/* Animations */
-@keyframes accordion-down {
- from {
- height: 0;
- }
- to {
- height: var(--radix-accordion-content-height);
- }
-}
-
-@keyframes accordion-up {
- from {
- height: var(--radix-accordion-content-height);
- }
- to {
- height: 0;
- }
-}