Skip to content

Instantly share code, notes, and snippets.

@johnhunter
Last active June 23, 2025 08:16
Show Gist options
  • Save johnhunter/0465ac1458d713a0def7de8a7507cf13 to your computer and use it in GitHub Desktop.
Save johnhunter/0465ac1458d713a0def7de8a7507cf13 to your computer and use it in GitHub Desktop.
React ModalDialog component

ModalDialog

This component uses the native html dialog element for modal heavy lifting.

API

The ModalDialog manages the display of the modal content and its transitions within the DOM. The actual stying of the modal content is delegated to the consuming component.

ModalDialog Props

  • open?: boolean
  • close: () => void
  • children: ReactNode

The open state is managed with the consuming component, useModalState provides sugar to simpify this.

useModalState signature

(initialState?: boolean) => [state:boolean, setOpen:()=>void, setClosed:()=>void]

Browser support

Support for the dialog element is good. The transitions on open/close are currently not supported by Firefox but degrades gracefully.

Example use

import ModalDialog, { useModalState } from '../ModalDialog';

const Foo = () => {
  const [modalOpen, setModalOpen, setModalClosed] = useModalState();
  return (
    <button onClick={setModalOpen}>Open</button>
    <ModalDialog open={modalOpen} close={setModalClosed}>
      <>
        Some card-like content...

        <button onClick={setModalClosed}>Close</button>
      </>
    </ModalDialog>
  );
}
export { default } from './ModalDialog';
export { useModalState } from './useModalState';
.modal {
--transition: all 400ms allow-discrete;
margin: auto; /* override resets */
background: transparent; /* styling is the content responsiblity */
min-width: clamp(320px, 60ch, 100vw); /* keep sensible widths */
opacity: 0;
transition: var(--transition);
&::backdrop {
background-color: rgb(0 0 0 / 0%);
transition: var(--transition);
}
&:open {
opacity: 1;
&::backdrop {
background-color: rgb(0 0 0 / 25%);
}
}
}
@starting-style {
.modal:open {
opacity: 0;
&::backdrop {
background-color: rgb(0 0 0 / 0%);
}
}
}
import { useEffect, useRef, type FC, type ReactNode } from 'react';
import css from './ModalDialog.module.css';
type ModalDialogProps = {
open?: boolean;
close: () => void;
children: ReactNode;
};
const ModalDialog: FC<ModalDialogProps> = ({ open, close, children }) => {
const ref = useRef<HTMLDialogElement>(null);
useEffect(() => {
if (open) {
ref.current?.showModal();
} else {
ref.current?.close();
}
}, [open]);
return (
<dialog ref={ref} onCancel={close} className={css.modal}>
{children}
</dialog>
);
};
export default ModalDialog;
import { useCallback, useState } from 'react';
/**
* A hook to toggle modal open state
*
* @returns a tuple [state, setOpen, setClosed]
*/
export const useModalState = (
initialState = false,
): [boolean, () => void, () => void] => {
const [state, setState] = useState(initialState);
const setOpen = useCallback(() => setState(true), [setState]);
const setClosed = useCallback(() => setState(false), [setState]);
return [state, setOpen, setClosed];
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment