Zoom Dropdown Menu
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.
Installation
Add the component via the Tiptap CLI:
npx @tiptap/cli@latest add zoom-dropdown-menuComponents
<ZoomDropdownMenu />
A prebuilt React component that renders the stepper buttons, the percentage trigger, and the popover with input, presets, and fit-to-page.
Usage
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
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.
import { clampZoom } from '@/components/tiptap-ui/zoom-dropdown-menu'
clampZoom(87.6) // 88
clampZoom(999) // 200parsePercent(input)
Parses a percentage string (e.g. "100%" or "100") to a number, or returns null for invalid input.
import { parsePercent } from '@/components/tiptap-ui/zoom-dropdown-menu'
parsePercent('125%') // 125
parsePercent('abc') // nullgetNextPresetZoom(currentZoom, direction, presets?, min?, max?)
Returns the next preset level above or below the current zoom.
import { getNextPresetZoom } from '@/components/tiptap-ui/zoom-dropdown-menu'
getNextPresetZoom(100, 'up') // 125
getNextPresetZoom(100, 'down') // 90Constants
ZOOM_MIN(40),ZOOM_MAX(200),ZOOM_DEFAULT(100)ZOOM_PRESETS—[40, 50, 75, 90, 100, 125, 150, 175, 200]ZoomLevel— the exportednumbertype 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)