---
title: mergeProps
subtitle: A utility to merge multiple sets of React props.
description: A utility to merge multiple sets of React props, handling event handlers, className, and style props intelligently.
---

# mergeProps

A utility to merge multiple sets of React props, handling event handlers, className, and style props intelligently.

`mergeProps` helps you combine multiple prop objects (for example, internal props + user props) into a single set of props you can spread onto an element.
It behaves like `Object.assign` (rightmost wins) with a few special cases, so common React patterns work as expected.

## How merging works

- For most keys (everything except `className`, `style`, and event handlers), the value from the rightmost object wins:
  ```ts title="returns { id: 'b', dir: 'ltr' }"
  mergeProps({ id: 'a', dir: 'ltr' }, { id: 'b' });
  ```
- `ref` is not merged. Only the rightmost ref is kept:
  ```ts title="only refB is used"
  mergeProps({ ref: refA }, { ref: refB });
  ```
- `className` values are concatenated right-to-left (rightmost first):
  ```ts title="className is 'b a'"
  mergeProps({ className: 'a' }, { className: 'b' });
  ```
- `style` objects are merged, with keys from the rightmost style overwriting earlier ones.
- Event handlers are merged and executed right-to-left (rightmost first):

  ```ts title="b runs before a"
  mergeProps({ onClick: a }, { onClick: b });
  ```

  - For React synthetic events, Base UI adds `event.preventBaseUIHandler()`. Calling it prevents Base UI's internal logic from running.
    This does not call `preventDefault()` or `stopPropagation()`.
  - For non-synthetic events (custom events with primitive/object values), this mechanism isn't available and all handlers always execute.

### Preventing Base UI's default behavior

## Demo

### CSS Modules

This example shows how to implement the component using CSS Modules.

```css
/* index.module.css */
.Container {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.ToggleRow {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.Label {
  font-size: 1rem;
  line-height: 1.5rem;
  color: var(--color-gray-900);
}

.Panel {
  display: flex;
  gap: 1px;
  border: 1px solid var(--color-gray-200);
  background-color: var(--color-gray-50);
  border-radius: 0.375rem;
  padding: 0.125rem;
}

.Button {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  margin: 0;
  outline: 0;
  border: 0;
  border-radius: 0.25rem;
  background-color: transparent;
  color: var(--color-gray-600);
  user-select: none;

  &:focus-visible {
    background-color: transparent;
    outline: 2px solid var(--color-blue);
    outline-offset: -1px;
  }

  @media (hover: hover) {
    &:hover {
      background-color: var(--color-gray-100);
    }
  }

  &:active {
    background-color: var(--color-gray-200);
  }

  &[data-pressed] {
    color: var(--color-gray-900);
  }
}

.Icon {
  width: 1.25rem;
  height: 1.25rem;
}

.LockButton {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 2.5rem;
  padding: 0 0.875rem;
  margin: 0;
  outline: 0;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  font-family: inherit;
  font-size: 1rem;
  font-weight: 500;
  line-height: 1.5rem;
  color: var(--color-gray-900);
  user-select: none;

  @media (hover: hover) {
    &:hover {
      background-color: var(--color-gray-100);
    }
  }

  &:active {
    background-color: var(--color-gray-100);
  }

  &:focus-visible {
    outline: 2px solid var(--color-blue);
    outline-offset: -1px;
  }
}
```

```tsx
/* index.tsx */
'use client';
import * as React from 'react';
import { mergeProps } from '@base-ui/react/merge-props';
import { Toggle } from '@base-ui/react/toggle';
import styles from './index.module.css';

export default function ExamplePreventBaseUIHandler() {
  const [locked, setLocked] = React.useState(true);
  const [pressed, setPressed] = React.useState(true);
  const getToggleProps = (props: React.ComponentProps<'button'>) =>
    mergeProps<'button'>(props, {
      onClick(event) {
        if (locked) {
          event.preventBaseUIHandler();
        }
      },
    });

  return (
    <div className={styles.Container}>
      <div className={styles.ToggleRow}>
        <div className={styles.Panel}>
          <Toggle
            aria-label="Favorite"
            pressed={pressed}
            onPressedChange={setPressed}
            className={styles.Button}
            render={(props, state) => (
              <button type="button" {...getToggleProps(props)}>
                {state.pressed ? (
                  <HeartFilledIcon className={styles.Icon} />
                ) : (
                  <HeartOutlineIcon className={styles.Icon} />
                )}
              </button>
            )}
          />
        </div>
        <span className={styles.Label}>Favorite {locked ? '(locked)' : '(unlocked)'}</span>
      </div>
      <button type="button" className={styles.LockButton} onClick={() => setLocked((l) => !l)}>
        {locked ? 'Unlock' : 'Lock'}
      </button>
    </div>
  );
}

function HeartFilledIcon(props: React.ComponentProps<'svg'>) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
      <path d="M7.99961 13.8667C7.88761 13.8667 7.77561 13.8315 7.68121 13.7611C7.43321 13.5766 1.59961 9.1963 1.59961 5.8667C1.59961 3.80856 3.27481 2.13336 5.33294 2.13336C6.59054 2.13336 7.49934 2.81176 7.99961 3.3131C8.49988 2.81176 9.40868 2.13336 10.6663 2.13336C12.7244 2.13336 14.3996 3.80803 14.3996 5.8667C14.3996 9.1963 8.56601 13.5766 8.31801 13.7616C8.22361 13.8315 8.11161 13.8667 7.99961 13.8667Z" />
    </svg>
  );
}

function HeartOutlineIcon(props: React.ComponentProps<'svg'>) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentcolor" {...props}>
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        d="M7.99961 4.8232L7.24456 4.06654C6.84123 3.66235 6.18866 3.20003 5.33294 3.20003C3.86391 3.20003 2.66628 4.39767 2.66628 5.8667C2.66628 6.4079 2.91276 7.1023 3.41967 7.91383C3.91548 8.70759 4.59649 9.51244 5.31278 10.2503C6.38267 11.3525 7.47318 12.2465 7.99983 12.6605C8.52734 12.2456 9.61718 11.352 10.6864 10.2504C11.4027 9.51248 12.0837 8.70762 12.5796 7.91384C13.0865 7.1023 13.3329 6.4079 13.3329 5.8667C13.3329 4.39723 12.1354 3.20003 10.6663 3.20003C9.81056 3.20003 9.15799 3.66235 8.75466 4.06654L7.99961 4.8232ZM7.98574 3.29926C7.48264 2.79938 6.57901 2.13336 5.33294 2.13336C3.27481 2.13336 1.59961 3.80856 1.59961 5.8667C1.59961 9.1963 7.43321 13.5766 7.68121 13.7611C7.77561 13.8315 7.88761 13.8667 7.99961 13.8667C8.11161 13.8667 8.22361 13.8315 8.31801 13.7616C8.56601 13.5766 14.3996 9.1963 14.3996 5.8667C14.3996 3.80803 12.7244 2.13336 10.6663 2.13336C9.42013 2.13336 8.51645 2.79947 8.01337 3.29936C8.00877 3.30393 8.00421 3.30849 7.99967 3.31303C7.99965 3.31305 7.99963 3.31307 7.99961 3.3131C7.99502 3.3085 7.9904 3.30389 7.98574 3.29926Z"
      />
    </svg>
  );
}
```

When using the function form of the `render` prop, props are not merged automatically.
You can use `mergeProps` to combine Base UI's props with your own, and call `preventBaseUIHandler()` to stop Base UI's internal logic from running:

## Passing a function instead of an object

Each argument can be a props object or a function that receives the merged props up to that point (left to right) and returns a props object.
This is useful when you need to compute the next props from whatever has already been merged.

Note that the function's return value completely replaces the accumulated props up to that point.
If you want to chain event handlers from the previous props, you must call them manually:

```tsx title="Manually chaining handlers in a function"
const merged = mergeProps(
  {
    onClick(event) {
      // Handler from previous props
    },
  },
  (props) => ({
    onClick(event) {
      // Manually call the previous handler
      props.onClick?.(event);
      // Your logic here
    },
  }),
);
```

## API reference

### mergeProps

This function accepts up to 5 arguments, each being either a props object or a function that returns a props object.
If you need to merge more than 5 sets of props, use `mergePropsN` instead.

Merges multiple sets of React props. It follows the Object.assign pattern where the rightmost object's fields overwrite
the conflicting ones from others. This doesn't apply to event handlers, `className` and `style` props.Event handlers are merged and called in right-to-left order (rightmost handler executes first, leftmost last).
For React synthetic events, the rightmost handler can prevent prior (left-positioned) handlers from executing
by calling `event.preventBaseUIHandler()`. For non-synthetic events (custom events with primitive/object values),
all handlers always execute without prevention capability.The `className` prop is merged by concatenating classes in right-to-left order (rightmost class appears first in the string).
The `style` prop is merged with rightmost styles overwriting the prior ones.Props can either be provided as objects or as functions that take the previous props as an argument.
The function will receive the merged props up to that point (going from left to right):
so in the case of `(obj1, obj2, fn, obj3)`, `fn` will receive the merged props of `obj1` and `obj2`.
The function is responsible for chaining event handlers if needed (i.e. we don't run the merge logic).Event handlers returned by the functions are not automatically prevented when `preventBaseUIHandler` is called.
They must check `event.baseUIHandlerPrevented` themselves and bail out if it's true.**`ref` is not merged.**

**Parameters:**

| Parameter | Type                      | Default | Description                                                                                    |
| :-------- | :------------------------ | :------ | :--------------------------------------------------------------------------------------------- |
| a         | `InputProps<ElementType>` | -       | Props object to merge.                                                                         |
| b         | `InputProps<ElementType>` | -       | Props object to merge. The function will overwrite conflicting props from `a`.                 |
| c?        | `InputProps<ElementType>` | -       | Props object to merge. The function will overwrite conflicting props from previous parameters. |
| d?        | `InputProps<ElementType>` | -       | Props object to merge. The function will overwrite conflicting props from previous parameters. |
| e?        | `InputProps<ElementType>` | -       | Props object to merge. The function will overwrite conflicting props from previous parameters. |

**Return Value:**

| Type | Description       |
| :--- | :---------------- |
| `{}` | The merged props. |

### mergePropsN

This function accepts an array of props objects or functions that return props objects.
It is slightly less efficient than `mergeProps`, so only use it when you need to merge more than 5 sets of props.

Merges an arbitrary number of React props using the same logic as \{@link mergeProps}.
This function accepts an array of props instead of individual arguments.
This has slightly lower performance than \{@link mergeProps}due to accepting an array
instead of a fixed number of arguments. Prefer \{@link mergeProps}when merging 5 or
fewer prop sets for better performance.

**Parameters:**

| Parameter | Type                        | Default | Description              |
| :-------- | :-------------------------- | :------ | :----------------------- |
| props     | `InputProps<ElementType>[]` | -       | Array of props to merge. |

**Return Value:**

| Type | Description       |
| :--- | :---------------- |
| `{}` | The merged props. |
