Skip to content

feat: migrate Dialog to Base UI#579

Merged
rohanchkrabrty merged 15 commits intomainfrom
base-dialog
Feb 16, 2026
Merged

feat: migrate Dialog to Base UI#579
rohanchkrabrty merged 15 commits intomainfrom
base-dialog

Conversation

@rohanchkrabrty
Copy link
Contributor

@rohanchkrabrty rohanchkrabrty commented Feb 2, 2026

Description

Changes

  • Component now uses Base UI Dialog primitive
  • Added transition for dialog and nested dialogs along with support for showNestedAnimation prop
  • Close button renders automatically via showCloseButton={true} (default)
  • Updated API to use render prop — replaced asChild with Base UI's render prop for Dialog.Trigger and Dialog.Close

Docs & Tests

  • Updated all dialog examples to remove CloseButton and use render prop
  • Updated props documentation with showNestedAnimation

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for nested dialogs with automatic backdrop handling and smooth animations.
    • New showNestedAnimation control for nested dialog transitions.
  • Refactor

    • Updated Dialog API: replaced asChild pattern with explicit render props for cleaner composition.
    • Consolidated overlay configuration into a single overlay object for simplified customization.
    • Renamed close prop to showCloseButton for clarity.
  • Documentation

    • Added comprehensive nested dialogs guide and examples.

@rohanchkrabrty rohanchkrabrty self-assigned this Feb 2, 2026
@vercel
Copy link

vercel bot commented Feb 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
apsara Ready Ready Preview, Comment Feb 16, 2026 6:10am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 4, 2026

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

This pull request comprehensively refactors the Dialog component system by extracting DialogContent into a dedicated module, consolidating overlay-related props into a single object, and replacing asChild patterns with render props. Nested dialog animation support and updated component organization are introduced.

Changes

Cohort / File(s) Summary
Core Dialog Architecture
packages/raystack/components/dialog/dialog.tsx, packages/raystack/components/dialog/dialog-content.tsx, packages/raystack/components/dialog/dialog-misc.tsx
Extracted DialogContent into separate module; moved DialogHeader, DialogFooter, DialogBody, CloseButton, DialogTitle, DialogDescription to dialog-misc.tsx; re-exported from main dialog.tsx for cohesive public API.
Dialog Styling
packages/raystack/components/dialog/dialog.module.css
Added nested dialog animation support with CSS variables, backdrop pseudo-element, close button positioning, viewport wrapper styles, and fade transitions; updated reduced-motion handling.
Dialog Props & API
apps/www/src/content/docs/components/dialog/props.ts
Removed flat overlay props (overlayBlur, overlayClassName, overlayStyle); added consolidated overlay object prop; added showCloseButton and showNestedAnimation flags; removed side and ARIA label props.
Dialog Examples & Demos
apps/www/src/components/playground/dialog-examples.tsx, apps/www/src/components/playground/code-block-examples.tsx, apps/www/src/content/docs/components/code-block/demo.ts, apps/www/src/content/docs/components/dialog/demo.ts
Updated all Dialog.Trigger usage from asChild to render prop pattern; replaced Dialog.Close asChild with render prop; consolidated overlay props into single object; removed Dialog.CloseButton from headers; added nestedDemo export.
Dialog Documentation
apps/www/src/content/docs/components/dialog/index.mdx
Added new nested dialogs section with nestedDemo import and documentation of nesting behavior.
Documentation Components
apps/www/src/components/docs/search.tsx, apps/www/src/components/demo/demo-playground.tsx
Updated Dialog.Trigger to use render prop pattern; added showCloseButton and showNestedAnimation props to Dialog.Content.
Example Pages
apps/www/src/app/examples/page.tsx
Removed Dialog.CloseButton from Dialog.Header.
Tests
packages/raystack/components/dialog/__tests__/dialog.test.tsx, packages/raystack/components/sheet/__tests__/sheet.test.tsx
Removed Dialog.CloseButton from Dialog.Header in dialog test; removed ARIA roles accessibility test from sheet test.
Sheet Component & Global
packages/raystack/components/sheet/sheet.tsx, apps/www/src/styles.css
Removed SheetRootProps export from public API; removed overscroll-behavior CSS rule from universal selector.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested labels

Do not merge

Suggested reviewers

  • rsbh
  • paanSinghCoder
  • rohilsurana

Poem

🐰 A dialog dance, we've rearranged the stage,
With render props now gracing every page,
Nested layers spin with animation's grace,
Overlay objects in a cleaner place,
The Dialog hops forward, restructured with care! 🎭✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: migrate Dialog to Base UI' directly and clearly summarizes the main change of the pull request—migrating the Dialog component to use the Base UI library.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch base-dialog

Comment @coderabbitai help to get the list of available commands and usage tips.

@rohanchkrabrty rohanchkrabrty marked this pull request as ready for review February 4, 2026 20:10
@rohanchkrabrty rohanchkrabrty requested review from paanSinghCoder and rsbh and removed request for rsbh February 4, 2026 20:13
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/www/src/components/playground/dialog-examples.tsx (1)

11-25: ⚠️ Potential issue | 🟡 Minor

Remove duplicate close button from the example.

Dialog.Content automatically renders a styled close button (CloseButton with icon and aria-label) by default since showCloseButton is not overridden. Line 24's <Dialog.Close /> adds a second, unstyled close button, creating duplicate close affordances. Remove it.

Suggested fix
-            <Dialog.Close />
🤖 Fix all issues with AI agents
In `@apps/www/src/components/playground/dialog-examples.tsx`:
- Around line 28-30: The Dialog.Trigger currently uses a render prop that
injects a Button (<Dialog.Trigger render={<Button variant='outline' />}>), but
also nests a <Button>Open Dialog</Button> as children which creates a
nested-button DOM; remove the nested Button child and replace it with plain text
(e.g., "Open Dialog") so the render prop provides the actual Button element and
the children become the trigger label; ensure only one Button instance is
present and keep references to Dialog.Trigger and Button when editing.

In `@apps/www/src/content/docs/components/dialog/demo.ts`:
- Around line 3-4: The getCode function can emit "undefined" when title or
description are missing; update the destructuring in getCode (or the template
interpolation) to default title and description to empty strings (e.g., const {
title = '', description = '' } = props or use fallback expressions when building
the template) so the generated snippet never contains the literal "undefined".
- Around line 145-179: When the parent dialog (parentOpen) closes we should
reset the nested dialog state (nestedOpen) to false so reopening the parent
doesn't auto-open the nested dialog; add a handler or useEffect in the component
that watches parentOpen (or inside setParentOpen wrapper) and calls
setNestedOpen(false) whenever parentOpen becomes false, referencing the existing
state setters setParentOpen and setNestedOpen used with the Dialog components.

In `@packages/raystack/components/dialog/dialog-content.tsx`:
- Around line 13-17: The JSDoc for the showNestedAnimation prop is backwards: it
currently says "Disables nested dialog animation" while the prop name and
`@default` true indicate it enables animation. Update the comment for
showNestedAnimation in dialog-content.tsx to correctly describe that it enables
nested dialog animations (scaling and translation) and keep `@default` true (or
adjust the default if intended otherwise); ensure the text references the
showNestedAnimation prop so consumers understand that true turns the nested
animation on.

Base automatically changed from base-sidebar to main February 4, 2026 20:33
rohanchkrabrty and others added 2 commits February 5, 2026 10:08
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/raystack/components/avatar/avatar.tsx (1)

196-200: ⚠️ Potential issue | 🟡 Minor

Potential runtime error with empty children array.

If children is an empty array, avatars[0] will be undefined, and getAvatarProps(undefined) will throw when accessing element.props. Consider adding a guard.

🛡️ Proposed defensive check
     const avatars = max ? children.slice(0, max) : children;
     const count = max && children.length > max ? children.length - max : 0;

     // Overflow avatar matches the first avatar styling
-    const firstAvatarProps = getAvatarProps(avatars[0]);
+    const firstAvatarProps = avatars[0] ? getAvatarProps(avatars[0]) : {};
apps/www/src/content/docs/components/dialog/props.ts (1)

69-83: ⚠️ Potential issue | 🔴 Critical

Update DialogTriggerProps and DialogCloseButtonProps to reflect Base UI's actual API.

Since Dialog.Trigger and Dialog.Close are direct exports from @base-ui/react, they use Base UI's render prop pattern for composition, not asChild. The props.ts documentation currently lists asChild?: boolean for both DialogTriggerProps (line 71) and DialogCloseButtonProps (line 79), which is inconsistent with the actual Base UI API.

Update these interfaces to include the render prop and remove asChild, matching Base UI's documented composition pattern.

🤖 Fix all issues with AI agents
In `@apps/www/src/content/docs/components/dialog/index.mdx`:
- Around line 82-85: Update the "Nested Dialogs" paragraph to state that the
parent dialog scaling and transparency only happen when the showNestedAnimation
prop is enabled (it defaults to true); mention that the nested dialog backdrop
is not rendered in this mode so the parent remains visible, and reference
showNestedAnimation explicitly so readers know this behavior is gated by that
prop.

In `@apps/www/src/content/docs/components/scroll-area/props.ts`:
- Around line 34-36: The render prop type for the ScrollArea component is
missing the second `state` parameter; update the `render?` union so the function
signature accepts `(props: React.HTMLAttributes<HTMLElement>, state:
ScrollArea.Root.State)` instead of only `props`, ensuring the type matches the
actual Base UI implementation (reference the `render` prop and
`ScrollArea.Root.State` types in the declaration).

In `@apps/www/src/content/docs/components/sidebar/props.ts`:
- Around line 15-18: The JSDoc for the collapsible prop is backwards: update the
comment for the collapsible?: boolean property so it correctly states that when
true the Sidebar can be collapsed/expanded (renders the collapse button) and
when false it disables that behavior; keep the `@default` true but change the
description from "Disable the click to collapse/expand the Sidebar" to something
like "Enable the click to collapse/expand the Sidebar" and include the behavior
note referencing the collapse button.

In `@apps/www/src/content/docs/components/switch/props.ts`:
- Around line 38-48: The render state type for the switch includes readOnly but
SwitchProps lacks a corresponding prop; update the type definition to be
consistent by either adding readOnly?: boolean to the SwitchProps interface (so
components can control it) or removing readOnly from the state union in the
render? declaration if the switch does not support a read-only mode; adjust
related places that reference SwitchProps or the render callback signature (look
for SwitchProps and the render? type in
apps/www/src/content/docs/components/switch/props.ts) to ensure the prop and
state stay in sync.

In `@packages/raystack/components/dialog/dialog-content.tsx`:
- Around line 39-46: The overlay object is being spread onto
DialogPrimitive.Backdrop which will pass the custom blur prop to the DOM;
destructure blur out of overlay first (e.g., const { blur, ...overlayProps } =
overlay || {}) and then spread overlayProps into DialogPrimitive.Backdrop while
using blur only for the conditional className (styles.overlayBlur) in the cx
call; update occurrences of {...overlay} to {...overlayProps} and keep
overlay?.className handling as-is to avoid leaking the blur attribute to the
DOM.
🧹 Nitpick comments (9)
packages/raystack/components/sidebar/__tests__/sidebar.test.tsx (1)

99-106: Consider adding a symmetric test for the open state.

The test verifies the collapsed state attributes but doesn't explicitly verify that data-open is present when the sidebar is expanded. Consider adding a complementary test for symmetry and completeness.

🧪 Suggested test addition
it('has data-open attribute when expanded', () => {
  render(<BasicSidebar open={true} />);

  const nav = screen.getByRole('navigation');
  expect(nav).toHaveAttribute('data-open');
  expect(nav).not.toHaveAttribute('data-closed');
});
packages/raystack/components/switch/__tests__/switch.test.tsx (1)

177-196: Consider updating controlled component test to match the new callback signature.

The onCheckedChange callback now receives (checked: boolean, event: Event), but this test passes setChecked directly. While this works (extra arguments are ignored), it doesn't demonstrate the actual API usage pattern. Consider using a wrapper function for consistency with other tests.

🔧 Suggested improvement
     it('works as controlled component', () => {
       const Component = () => {
         const [checked, setChecked] = React.useState(false);
         return (
           <Switch
             checked={checked}
-            onCheckedChange={setChecked}
+            onCheckedChange={(newChecked) => setChecked(newChecked)}
             data-testid='controlled'
           />
         );
       };
apps/www/src/content/docs/components/checkbox/demo.ts (1)

5-8: Optional: remove legacy string coercion in getCode.
With checkbox controls now boolean, these conversions appear unnecessary.

♻️ Suggested cleanup
 export const getCode = (props: any) => {
-  if (props.checked === 'false') props.checked = false;
-  else if (props.checked === 'true') props.checked = true;
-
   return `<Checkbox${getPropsString(props)}/>`;
 };
packages/raystack/components/separator/__tests__/separator.test.tsx (1)

88-92: Test duplicates assertion from "Orientation" test suite.

The assertion expect(separator).toHaveAttribute('aria-orientation', 'horizontal') is already verified in the "defaults to horizontal orientation" test at lines 29-34. Consider either:

  1. Removing this test to avoid redundancy, or
  2. Renaming and modifying it to test something distinct (e.g., verifying aria-orientation is always present regardless of the orientation prop value)
apps/www/src/content/docs/components/radio/props.ts (2)

3-9: Prefer specific types over any for value props.

Using any for defaultValue, value, and the onValueChange parameter removes type safety and provides no guidance to consumers. Radio values are typically strings.

Consider using string or a generic type parameter for better type safety in the documentation.

♻️ Suggested type improvement
-  /** The value of the radio item that should be checked by default. */
-  defaultValue?: any;
+  /** The value of the radio item that should be checked by default. */
+  defaultValue?: string;

   /** The controlled value of the radio item that is checked. */
-  value?: any;
+  value?: string;

   /** Event handler called when the value changes. */
-  onValueChange?: (value: any, event: Event) => void;
+  onValueChange?: (value: string, event: Event) => void;

31-33: Same type safety concern for RadioProps value.

The value prop should use string (or match the group's type) instead of any for consistency and type safety.

♻️ Suggested fix
 export interface RadioProps {
   /** The unique value of the radio item. */
-  value: any;
+  value: string;
packages/raystack/components/dialog/dialog.module.css (2)

23-34: Consider narrowing the transition property.

Using transition: all 150ms on Line 26 will animate any property change (including width, height, background-color, etc.), which may cause unintended visual effects. Consider specifying only the properties that should animate.

♻️ Suggested refinement
-.dialogContent {
-  transition: all 150ms;
+.dialogContent {
+  transition: opacity 150ms, transform 150ms, translate 150ms;

30-34: Mixing transform and translate properties.

The .showNestedAnimation class uses both the transform property (with scale()) and the separate translate property. While this is valid modern CSS, it can be confusing since both affect element positioning. The translate property won't be included in the transform calculation and operates independently.

This works correctly but consider documenting this intentional separation or consolidating into a single transform for clarity.

packages/raystack/components/dialog/dialog-misc.tsx (1)

55-71: Consider consolidating CloseButton implementations.

There's a similar CloseButton in packages/raystack/components/sheet/sheet.tsx (Lines 98-112) that also wraps DialogPrimitive.Close. Both serve the same purpose but have slightly different implementations. Consider extracting a shared CloseButton component to reduce duplication.

Comment on lines 15 to 18
/** Disable the click to collapse/expand the Sidebar.
* @default undefined
* @default true
*/
collapsible?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n -C5 'collapsible' --type=ts --type=tsx -g '*sidebar*' -g '!**/props.ts'

Repository: raystack/apsara

Length of output: 86


🏁 Script executed:

rg -n -C5 'collapsible' --type=ts -g '*sidebar*' -g '!**/props.ts'

Repository: raystack/apsara

Length of output: 10190


Update the sidebar collapsible prop documentation to match its actual behavior.

The prop name collapsible and the default value of true indicate that when enabled, the sidebar can be collapsed/expanded. However, the current description ("Disable the click to collapse/expand the Sidebar") is backwards. The implementation confirms that collapsible: true enables the collapse/expand functionality (renders the button), while collapsible: false disables it (hides the button).

📝 Suggested documentation fix
-  /** Disable the click to collapse/expand the Sidebar.
+  /** Enable the sidebar to be collapsed/expanded via click.
    * `@default` true
    */
   collapsible?: boolean;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Disable the click to collapse/expand the Sidebar.
* @default undefined
* @default true
*/
collapsible?: boolean;
/** Enable the sidebar to be collapsed/expanded via click.
* `@default` true
*/
collapsible?: boolean;
🤖 Prompt for AI Agents
In `@apps/www/src/content/docs/components/sidebar/props.ts` around lines 15 - 18,
The JSDoc for the collapsible prop is backwards: update the comment for the
collapsible?: boolean property so it correctly states that when true the Sidebar
can be collapsed/expanded (renders the collapse button) and when false it
disables that behavior; keep the `@default` true but change the description from
"Disable the click to collapse/expand the Sidebar" to something like "Enable the
click to collapse/expand the Sidebar" and include the behavior note referencing
the collapse button.

Comment on lines +39 to +46
<DialogPrimitive.Backdrop
{...overlay}
className={cx(
styles.dialogOverlay,
overlay?.blur && styles.overlayBlur,
overlay?.className
)}
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential prop leak: blur passed to DOM element.

The overlay object is spread directly onto DialogPrimitive.Backdrop, but blur is a custom prop used only for conditional className. This will pass blur as an attribute to the underlying DOM element, causing a React warning for unknown props.

🛠️ Suggested fix: destructure blur before spreading
   (
     {
       className,
       children,
       showCloseButton = true,
       overlay,
       width,
       style,
       showNestedAnimation = true,
       ...props
     },
     ref
   ) => {
+    const { blur, ...overlayProps } = overlay ?? {};
     return (
       <DialogPrimitive.Portal>
         <DialogPrimitive.Backdrop
-          {...overlay}
+          {...overlayProps}
           className={cx(
             styles.dialogOverlay,
-            overlay?.blur && styles.overlayBlur,
+            blur && styles.overlayBlur,
             overlay?.className
           )}
         />
🤖 Prompt for AI Agents
In `@packages/raystack/components/dialog/dialog-content.tsx` around lines 39 - 46,
The overlay object is being spread onto DialogPrimitive.Backdrop which will pass
the custom blur prop to the DOM; destructure blur out of overlay first (e.g.,
const { blur, ...overlayProps } = overlay || {}) and then spread overlayProps
into DialogPrimitive.Backdrop while using blur only for the conditional
className (styles.overlayBlur) in the cx call; update occurrences of
{...overlay} to {...overlayProps} and keep overlay?.className handling as-is to
avoid leaking the blur attribute to the DOM.

@rohanchkrabrty rohanchkrabrty merged commit f50ec6f into main Feb 16, 2026
5 checks passed
@rohanchkrabrty rohanchkrabrty deleted the base-dialog branch February 16, 2026 07:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants