---
title: "Zoom Dropdown Menu"
description: "Add a controlled zoom control with steppers, presets, a numeric input, and fit-to-page to your editor UI. More in the documentation!"
canonical_url: "https://tiptap-docs.lin52025i.qzz.io/ui-components/components/zoom-dropdown-menu"
---

# Zoom Dropdown Menu

Add a controlled zoom control with steppers, presets, a numeric input, and fit-to-page to your editor UI. More in the documentation!

A controlled zoom control for paginated or canvas-style editors. It pairs zoom-out / zoom-in stepper buttons with a percentage trigger whose popover offers a numeric input, preset levels, and an optional “Fit to page” action.

The component is fully **headless and presentational** — it holds no zoom state of its own and has **no Tiptap editor dependency**. You own the zoom value and pass it in through `currentZoom` / `onZoomChange`, so it works with any zoom implementation. All of the control logic lives in the `useZoomDropdownMenu` hook, making the built-in UI just one possible renderer.

> **Interactive demo:** [zoom dropdown menu](https://template.tiptap.dev/preview/tiptap-ui/zoom-dropdown-menu)

## Installation

Add the component via the Tiptap CLI:

```bash
npx @tiptap/cli@latest add zoom-dropdown-menu
```

## Components

### `<ZoomDropdownMenu />`

A prebuilt React component that renders the stepper buttons, the percentage trigger, and the popover with input, presets, and fit-to-page.

#### Usage

```tsx
import { useState } from 'react'
import { ZoomDropdownMenu, type ZoomLevel } from '@/components/tiptap-ui/zoom-dropdown-menu'

export default function MyToolbar() {
  const [zoom, setZoom] = useState<ZoomLevel>(100)

  return (
    <ZoomDropdownMenu currentZoom={zoom} onZoomChange={setZoom} onFitToPage={() => setZoom(100)} />
  )
}
```

#### Props

The component also forwards any extra `Button` props (except `type`) to the percentage trigger.

| Name           | Type                     | Default        | Description                                                         |
| -------------- | ------------------------ | -------------- | ------------------------------------------------------------------- |
| `currentZoom`  | `number`                 | `100`          | The current zoom level (percentage). Controlled by you.             |
| `onZoomChange` | `(zoom: number) => void` | `undefined`    | Called with the next zoom level whenever the user changes it.       |
| `onFitToPage`  | `() => void`             | `undefined`    | Called when “Fit to page” is triggered. Omit to hide that action.   |
| `min`          | `number`                 | `40`           | Smallest selectable zoom level.                                     |
| `max`          | `number`                 | `200`          | Largest selectable zoom level.                                      |
| `presets`      | `readonly number[]`      | `ZOOM_PRESETS` | Preset levels offered in the picker and used when stepping up/down. |

## Hooks

### `useZoomDropdownMenu()`

A headless hook that derives everything a zoom UI needs. The consumer owns the value (`currentZoom` / `onZoomChange`); the hook computes the clamped value, min/max flags, the text field state with parse-and-commit, keyboard handling, preset stepping, and the fit-to-page passthrough.

#### Usage

```tsx
function MyZoomControl(props) {
  const { zoom, zoomIn, zoomOut, isMinZoom, isMaxZoom } = useZoomDropdownMenu(props)

  return (
    <div>
      <button onClick={zoomOut} disabled={isMinZoom}>
        -
      </button>
      <span>{zoom}%</span>
      <button onClick={zoomIn} disabled={isMaxZoom}>
        +
      </button>
    </div>
  )
}
```

#### Props

| Name           | Type                     | Default        | Description                                             |
| -------------- | ------------------------ | -------------- | ------------------------------------------------------- |
| `currentZoom`  | `number`                 | `100`          | The current zoom level (percentage). Controlled by you. |
| `onZoomChange` | `(zoom: number) => void` | `undefined`    | Called with the next zoom level whenever it changes.    |
| `onFitToPage`  | `() => void`             | `undefined`    | Called when fit-to-page is triggered. Omit to hide it.  |
| `min`          | `number`                 | `40`           | Smallest selectable zoom level.                         |
| `max`          | `number`                 | `200`          | Largest selectable zoom level.                          |
| `presets`      | `readonly number[]`      | `ZOOM_PRESETS` | Preset levels offered and used for stepping.            |

#### Return Values

| Name                 | Type                                                 | Description                                                            |
| -------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------- |
| `zoom`               | `number`                                             | The clamped current zoom level.                                        |
| `min`                | `number`                                             | The effective minimum zoom.                                            |
| `max`                | `number`                                             | The effective maximum zoom.                                            |
| `presets`            | `readonly number[]`                                  | The preset levels.                                                     |
| `isMinZoom`          | `boolean`                                            | Whether the current zoom is at (or below) `min`.                       |
| `isMaxZoom`          | `boolean`                                            | Whether the current zoom is at (or above) `max`.                       |
| `canFitToPage`       | `boolean`                                            | Whether a fit-to-page action is available (`onFitToPage` provided).    |
| `zoomIn`             | `() => void`                                         | Step to the next preset above the current zoom.                        |
| `zoomOut`            | `() => void`                                         | Step to the next preset below the current zoom.                        |
| `setZoom`            | `(zoom: number) => void`                             | Apply an arbitrary level (clamped to `min`/`max`).                     |
| `inputValue`         | `string`                                             | Controlled value for a percentage text field.                          |
| `setInputValue`      | `(value: string) => void`                            | Setter for the text field value.                                       |
| `commitInput`        | `() => void`                                         | Parse and apply the text field (reverts if invalid). Wire to `onBlur`. |
| `handleInputKeyDown` | `(e: React.KeyboardEvent<HTMLInputElement>) => void` | Enter/Escape handling for the text field. Wire to `onKeyDown`.         |
| `fitToPage`          | `() => void`                                         | Invoke the consumer’s fit-to-page handler.                             |

## Utilities

The component module also exports a few pure helpers and constants for building custom zoom logic.

### `clampZoom(value, min?, max?)`

Clamps a zoom value to the supported range, rounded to the nearest integer.

```tsx
import { clampZoom } from '@/components/tiptap-ui/zoom-dropdown-menu'

clampZoom(87.6) // 88
clampZoom(999) // 200
```

### `parsePercent(input)`

Parses a percentage string (e.g. `"100%"` or `"100"`) to a number, or returns `null` for invalid input.

```tsx
import { parsePercent } from '@/components/tiptap-ui/zoom-dropdown-menu'

parsePercent('125%') // 125
parsePercent('abc') // null
```

### `getNextPresetZoom(currentZoom, direction, presets?, min?, max?)`

Returns the next preset level above or below the current zoom.

```tsx
import { getNextPresetZoom } from '@/components/tiptap-ui/zoom-dropdown-menu'

getNextPresetZoom(100, 'up') // 125
getNextPresetZoom(100, 'down') // 90
```

### Constants

- `ZOOM_MIN` (`40`), `ZOOM_MAX` (`200`), `ZOOM_DEFAULT` (`100`)
- `ZOOM_PRESETS` — `[40, 50, 75, 90, 100, 125, 150, 175, 200]`
- `ZoomLevel` — the exported `number` type alias used across the API

## Requirements

### Dependencies

- `react` — the control is presentational and does not depend on Tiptap.

### Referenced Components

- `button` (primitive)
- `button-group` (primitive)
- `card` (primitive)
- `input` (primitive)
- `popover` (primitive)
- `separator` (primitive)
- `fullscreen-icon` (icon)
- `minus-icon` (icon)
- `plus-icon` (icon)
