- Published on
Triggering a Radix Dialog or ShadCN Dialog Without a Button
- Authors
- Name
- Ripal & Zalak
Introduction
Radix UI and ShadCN UI provide accessible dialog components with robust functionality. However, attempting to use a custom React component as a trigger instead of the default button can result in hydration errors in Next.js applications. This guide demonstrates how to achieve your desired behavior while addressing these errors.
The Problem
When using a custom component (e.g., a markdown editor) as a trigger for a ShadCN Dialog, the following errors often occur:
- Hydration failed because the initial UI does not match what was rendered on the server.
- Expected server HTML to contain a matching
<button>
in<div>
.
This happens because Radix UI’s DialogTrigger
expects a button
element, which is essential for accessibility and consistency. Passing a non-standard element (like a React component) can cause mismatches between server-rendered and client-rendered HTML.
Solution
To resolve these issues, you need to:
- Ensure the custom component renders as a valid
button
element. - Use
DialogTrigger
correctly with a controlledopen
state to manage the dialog. - Handle interactions without breaking Radix UI’s accessibility features.
Step 1: Modify the Custom Component to Render as a Button
Ensure your custom component supports rendering as a button
by adding an asChild
prop to DialogTrigger
. This allows you to use any valid React component as the trigger while maintaining accessibility and avoiding hydration issues.
Example:
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from 'shadcn-ui/dialog'
import React, { useState } from 'react'
const LLEditor = ({ onClick }: { onClick?: () => void }) => {
return (
<div
role="button"
tabIndex={0}
onClick={onClick}
className="cursor-pointer rounded-lg border p-2"
>
Click to edit
</div>
)
}
const DialogExample = () => {
const [open, setOpen] = useState(false)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<LLEditor onClick={() => setOpen(true)} />
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Content</DialogTitle>
<DialogDescription>Modify the content below:</DialogDescription>
</DialogHeader>
<div className="mt-4">Here’s your editable content.</div>
</DialogContent>
</Dialog>
)
}
export default DialogExample
Step 2: Use Controlled State for the Dialog
Manage the dialog’s open state explicitly. This ensures the server-rendered and client-rendered HTML match:
- Use the
open
prop ofDialog
to control the visibility of the dialog. - Use
setOpen
to toggle the dialog state.
Step 3: Ensure Accessibility
To maintain accessibility, ensure that the custom trigger component:
- Renders with a
role="button"
. - Has a
tabIndex={0}
to allow keyboard navigation. - Supports
onClick
andonKeyDown
handlers for keyboard accessibility.
Common Mistakes to Avoid
- Skipping the
asChild
Prop: WithoutasChild
, Radix UI wraps your component in abutton
, causing hydration mismatches. - Forgetting Accessibility Attributes: Custom components should include
role
,tabIndex
, and ARIA attributes where necessary. - Not Using Controlled State: Relying solely on
DialogTrigger
withoutopen
andonOpenChange
can lead to inconsistent behavior.
Complete Example
Here’s a full example combining all the steps:
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from 'shadcn-ui/dialog'
import React, { useState } from 'react'
const LLEditor = ({ onClick }: { onClick?: () => void }) => {
return (
<div
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={(e) => e.key === 'Enter' && onClick?.()}
className="cursor-pointer rounded-lg border p-2"
>
Click to edit
</div>
)
}
const DialogExample = () => {
const [open, setOpen] = useState(false)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<LLEditor onClick={() => setOpen(true)} />
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Content</DialogTitle>
<DialogDescription>Modify the content below:</DialogDescription>
</DialogHeader>
<div className="mt-4">Here’s your editable content.</div>
</DialogContent>
</Dialog>
)
}
export default DialogExample
Conclusion
Using a custom React component as a trigger for a Radix or ShadCN dialog is possible without hydration errors. By ensuring your component behaves like a button
, using the asChild
prop, and managing state explicitly, you can achieve the desired behavior while maintaining accessibility.