<dialog> elementtl;dr
Using the built-in dialog.showModal() causes the <dialog> element to always be on top, so toast notification are hidden behind the ::backdrop.
Not too long ago, the <dialog> element received some nice functionality for showing and closing it easily with JavaScript. It also came along with a new ::backdrop pseudo-element for styling the backdrop overlay when the dialog is open.
Most of my projects use Next.js with Tailwind CSS. Because Next.js is built on React, I can take advantage of the Headless UI package by Tailwind Labs, and I have previously used their <Dialog> component instead of the <dialog> element. In this project, however, I decided to try the built-in <dialog> element functionality, for it's accessibility features.
In this case, I am using the dialog to allow users to change their password. Naturally, it's important to display notification to the user, to let them know whether the password was changed successfully. I'm using React Hot Toast to handle these notifications, of course. In previous projects, when using Headless UI's <Dialog> component, I haven't had any issues displaying these notifications.
With the built-in <dialog> element the notifications are stuck behind it. This isn't a big issue if the ::backdrop pseudo-element is transparent, but it always looks better if it is.
I've already tested changing the z-index on both the <dialog> element and the <Toaster> component, via Tailwind classes and the style attribute. Neither work, even with extreme values.
Keep in mind, I'm using the App Router.
<RootLayout><Toaster
position='top-right'
toastOptions={{
duration: 3000
}}
/>
<ChangePassword> Componentfunction ChangePassword({ username }: { username: string | undefined }) {
const changePasswordDialogRef = useRef<HTMLDialogElement>(null),
currentPasswordInputRef = useRef<HTMLInputElement>(null),
newPasswordInputRef = useRef<HTMLInputElement>(null),
formRef = useRef<HTMLFormElement>(null)
const currentPasswordValueRef = useRef(''),
newPasswordValueRef = useRef('')
const currentPasswordIsValidRef = useRef(false),
newPasswordIsValidRef = useRef(false)
const openPasswordChangeModal = () => changePasswordDialogRef.current?.showModal()
const closePasswordChangeModal = () => {
formRef.current?.reset()
changePasswordDialogRef.current?.close()
}
const [formSubmissionState, setFormSubmissionState] = useState<
'incomplete' | 'ready' | 'loading' | 'success' | 'error'
>('incomplete')
const validateInputs = useCallback(
(inputId: string, newValue: string) => {
if (
(inputId === 'currentPassword' && newValue.length >= 8) ||
(inputId !== 'currentPassword' &&
currentPasswordValueRef.current.length >= 8) ||
getComputedStyle(currentPasswordInputRef.current as Element).position === 'static'
) {
currentPasswordIsValidRef.current = true
} else {
currentPasswordIsValidRef.current = false
}
if (
(inputId === 'newPassword' && newValue.length >= 8) ||
(inputId !== 'newPassword' &&
newPasswordValueRef.current.length >= 8) ||
getComputedStyle(newPasswordInputRef.current as Element).position === 'static'
) {
newPasswordIsValidRef.current = true
} else {
newPasswordIsValidRef.current = false
}
const currentPasswordIsValid = currentPasswordIsValidRef.current,
newPasswordIsValid = newPasswordIsValidRef.current
const validInputs = [currentPasswordIsValid, newPasswordIsValid]
const inputsAreValid = validInputs.includes(false) ? false : true
if (inputsAreValid && formSubmissionState !== 'ready')
setFormSubmissionState('ready')
if (!inputsAreValid && formSubmissionState !== 'incomplete')
setFormSubmissionState('incomplete')
},
[formSubmissionState]
)
let inputChangeTimeout: NodeJS.Timeout
const handleInputChange: ChangeEventHandler<HTMLInputElement> = e => {
clearTimeout(inputChangeTimeout)
inputChangeTimeout = setTimeout(async () => {
const { id, value } = e.target
switch (id) {
case 'currentPassword':
currentPasswordValueRef.current = value
break
case 'newPassword':
newPasswordValueRef.current = value
break
default:
null
}
validateInputs(id, value)
}, 250)
}
const handleChangePassword = async () => {
const currentPassword = currentPasswordValueRef.current,
newPassword = newPasswordValueRef.current
if (currentPassword === newPassword) {
toast.error('You cannot set the new password to your old password.')
throw new Error('You cannot set the new password to your old password.')
}
return true
}
const toastChangePassword: FormEventHandler<HTMLFormElement> = e => {
e.preventDefault()
e.stopPropagation()
toast.promise(handleChangePassword(), {
loading: 'Saving Password...',
success: () => {
closePasswordChangeModal()
return 'Successfully changed password.'
},
error: 'Failed to change password. Please try again.'
})
}
return (
<>
<div className='w-fit sm:ml-auto -mt-2'>
<a
href='#top'
onClick={openPasswordChangeModal}
>
Change Password
</a>
</div>
<dialog
ref={changePasswordDialogRef}
className='
w-[calc(100%-2rem)] max-w-xs
rounded-3xl
shadow-xl dark:shadow-zinc-700
backdrop:bg-zinc-100/60 dark:backdrop:bg-zinc-900/60
backdrop:backdrop-blur-sm dark:backdrop:brightness-125
'
>
<form
ref={formRef}
onSubmit={toastChangePassword}
>
<input
ref={currentPasswordInputRef}
name='Current Password'
type='password'
placeholder='••••••••'
className='placeholder:font-black placeholder:tracking-widest autofill:static'
minLength={8}
onChange={handleInputChange}
autoComplete='old-password'
required
/>
<input
ref={newPasswordInputRef}
name='New Password'
type='password'
placeholder='••••••••'
className='placeholder:font-black placeholder:tracking-widest autofill:static'
minLength={8}
onChange={handleInputChange}
autoComplete='new-password'
required
/>
<button type='submit'>Update Password</button>
</form>
</dialog>
</>
)
}
I'm using slightly different packages, but ran into the same issue with react-hot-toast that I solved.
I'm using material-tailwind Dialog with react-hot-toast in a Next.js app. I found that setting the containerStyle prop in the Toaster component to {{zIndex: 99999}} moved the toast in front of the Dialog background blur so that the toast alert was in focus. I tried up to 9999 for the value of zIndex and it still didn't work there, but 99999 works for me.
react-hot-toast styling can be found here: https://react-hot-toast.com/docs/styling
The full Toaster component that worked for me is here which is placed in RootLayout in layout.tsx:
<Toaster
position="bottom-left"
containerStyle={{zIndex: 99999}}
toastOptions={{
success: {
style: {
background: 'lightblue',
},
iconTheme: {
primary: 'white',
secondary: 'black',
}
},
error: {
style: {
background: 'palevioletred',
},
},
}}
/>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With