tailwindlabs/headlessui · critical · Error

You provided an `onClose` prop to the `Dialog`, but the valu

Error message

You provided an `onClose` prop to the `Dialog`, but the value is not a function. Received: ${props.onClose}

What it means

Headless UI's Dialog is a controlled component: it requires an onClose callback prop so it can inform you when the user requests the dialog to close (Escape key, backdrop click, focus-trap interactions). During render, DialogFn validates that props.onClose is actually a function and throws immediately if not. This is a fail-fast guard because a missing onClose leaves the dialog permanently stuck open.

Source

Thrown at packages/@headlessui-react/src/components/dialog/dialog.tsx:411

    throw new Error(
      `You provided an \`onClose\` prop to the \`Dialog\`, but forgot an \`open\` prop.`
    )
  }

  if (!hasOnClose) {
    throw new Error(
      `You provided an \`open\` prop to the \`Dialog\`, but forgot an \`onClose\` prop.`
    )
  }

  if (!usesOpenClosedState && typeof props.open !== 'boolean') {
    throw new Error(
      `You provided an \`open\` prop to the \`Dialog\`, but the value is not a boolean. Received: ${props.open}`
    )
  }

  if (typeof props.onClose !== 'function') {
    throw new Error(
      `You provided an \`onClose\` prop to the \`Dialog\`, but the value is not a function. Received: ${props.onClose}`
    )
  }

  if ((open !== undefined || transition) && !rest.static) {
    return (
      <MainTreeProvider>
        <Transition show={open} transition={transition} unmount={rest.unmount}>
          <InternalDialog ref={ref} {...rest} />
        </Transition>
      </MainTreeProvider>
    )
  }

  return (
    <MainTreeProvider>
      <InternalDialog ref={ref} open={open} {...rest} />
    </MainTreeProvider>

View on GitHub (pinned to eea57cf46f)

Solutions

  1. Add (or fix) the onClose prop: <Dialog open={isOpen} onClose={() => setIsOpen(false)}>
  2. If conditionally rendering props, ensure onClose is always a function, e.g. onClose={handleClose ?? (() => {})}
  3. Check that the handler you pass actually exists (no typos like onclose or onCloseHandler undefined at runtime)
  4. In TypeScript, don't cast props to any when wrapping Dialog — keep the required onClose in your wrapper's own props

Example fix

// before
<Dialog open={isOpen}>
  <Dialog.Panel>...</Dialog.Panel>
</Dialog>

// after
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
  <Dialog.Panel>...</Dialog.Panel>
</Dialog>
Defensive patterns

Strategy: type-guard

Validate before calling

const canRenderDialog = typeof onClose === 'function' && typeof open === 'boolean';
return canRenderDialog ? <Dialog open={open} onClose={onClose}>...</Dialog> : null;

Type guard

const isDialogProps = (p) =>
  typeof p.open === 'boolean' && typeof p.onClose === 'function';

Prevention

When it happens

Trigger: Rendering <Dialog open={...}> without an onClose prop; passing onClose={null} or onClose={undefined} (e.g. conditionally spreading props); passing a value like onClose={dialogState.close} where dialogState is undefined so the value evaluates to undefined; passing a string or other non-function by typo.

Common situations: Converting an uncontrolled example to TypeScript and dropping the prop; copying Dialog from docs but forgetting the close handler; spreading {...rest} that overwrites onClose with undefined; version upgrades where prop types became stricter (onClose required).

Related errors


AI-assisted analysis of tailwindlabs/headlessui@eea57cf46f (2026-08-28). Data as JSON: /api/errors/00518b45599abee8. Report an issue: GitHub.