Customizable React Zoom Controller
https://github.com/canerdemirci/react-zoom-controller/
https://www.npmjs.com/package/react-zoom-controller
I have written a zoom controller in React and published on npm. I wanted this component to be more useful than the HTML slider element. Thanks to the dropdown menu, you can set more zoom levels like 200% and 400%, and quickly input any number. I can summarize the component's properties like this:
Zoom slider step size
Zoom slider points (25%, 50%, 75%, 100%)
Zoom slider width
Zoom slider tooltip (optional)
Zoom options (dropdown menu - accepts zoom value input) (optional)
Customizable style
Example of using the component:
I used css scale for zoom but you can use zoom property
<img src={logo} style={{ transform: `scale(${zoom}%)`, transformOrigin: 'center' }} />
import { useState } from 'react'
import './App.css'
import ZoomController from 'react-zoom-controller'
import logo from './assets/logo.png'
function App() {
const [zoom, setZoom] = useState<number>(50)
function handleZoomControllerOnChange(value: number) {
setZoom(value)
}
return (
<div className="App">
<div className="logoContainer">
<h1>React Zoom Controller</h1>
<img src={logo} style={{ transform: `scale(${zoom}%)`, transformOrigin: 'center' }} />
</div>
<div className="controllersSection">
<ZoomController
value={zoom}
onChange={handleZoomControllerOnChange}
/>
<ZoomController
value={zoom}
onChange={handleZoomControllerOnChange}
sliderWidth={100}
sliderStyle={{
trackColor: 'lightgreen',
valueTrackColor: 'darkgreen',
thumbColor: 'darkgreen',
tooltipColor: 'green',
tooltipTextColor: 'white'
}}
selectBoxStyle={{
borderColor: 'darkgreen',
textColor: 'white',
backgroundColor: 'green',
downArrowColor: 'white',
optionBackgroundColor: 'teal',
optionTextColor: 'white'
}}
/>
<ZoomController
value={zoom}
onChange={handleZoomControllerOnChange}
sliderWidth={200}
sliderStyle={{
trackColor: 'magenta',
valueTrackColor: 'blue',
thumbColor: 'blue',
tooltipColor: 'royalblue',
tooltipTextColor: 'white'
}}
selectBoxStyle={{
borderColor: 'magenta',
textColor: 'purple',
optionBackgroundColor: 'magenta',
optionTextColor: 'white'
}}
/>
<ZoomController
value={zoom}
onChange={handleZoomControllerOnChange}
sliderStepSize={10}
sliderWidth={300}
sliderStyle={{
trackColor: 'orange',
valueTrackColor: 'red',
thumbColor: 'red',
tooltipColor: 'orange',
tooltipTextColor: 'darkred'
}}
selectBoxStyle={{
borderColor: 'red',
textColor: 'darkred',
backgroundColor: 'orange',
downArrowColor: 'darkred',
optionBackgroundColor: 'red',
optionTextColor: 'white'
}}
/>
<ZoomController
value={zoom}
onChange={handleZoomControllerOnChange}
sliderWidth={250}
sliderStepSize={25}
sliderStyle={{
trackColor: 'lightblue',
valueTrackColor: 'teal',
thumbColor: 'teal',
tooltipColor: 'lightblue',
tooltipTextColor: 'teal'
}}
selectBoxStyle={{
borderColor: 'teal',
textColor: 'teal',
backgroundColor: 'lightblue',
downArrowColor: 'teal',
optionBackgroundColor: 'teal',
optionTextColor: 'white'
}}
/>
</div>
</div>
);
}
export default App;
Here are the component's slider codes. You can check the comments for more details:
// Slider types
export interface ISlider {
value?: number
stepSize?: number
sliderWidth?: number
onChange: (value: number) => void
toolTipVisibility?: boolean
sliderStyle?: ISliderStyle
}
export type ISliderStyle = {
valueTrackColor?: string
trackColor?: string
thumbColor?: string
tooltipColor?: string
tooltipTextColor?: string
}
import { useEffect, useState } from 'react'
import { ISlider } from './index.types'
import styles from './styles.module.css'
// Initial values
const DEFAULT_VALUE = 100, SLIDER_WIDTH = 150, THUMB_SIZE = 20, STEP_SIZE = 1
// For centering
const calculateThumbPosX = (sliderValue: number, sliderWidth: number): number =>
sliderValue - (THUMB_SIZE / 2 * 100 / sliderWidth)
const calculateToolTipPosX = (sliderValue: number, sliderWidth: number): number =>
sliderValue - (44 / 2 * 100 / sliderWidth)
export default function Slider({
// Percentage 0-100 and 0-500
value = DEFAULT_VALUE,
stepSize = STEP_SIZE,
sliderWidth = SLIDER_WIDTH,
onChange,
toolTipVisibility = true,
sliderStyle
}: ISlider) {
// It is needed to calculate mouse x position (percentage) on slider
const sliderRect = document
.getElementById('zoomContSlider')
?.getBoundingClientRect()
// Mouse x position on viewport
const [clientX, setClientX] = useState<number>(0)
useEffect(() => {
const cvalue = calculateValue(clientX)
// Update Slider value when it divided exactly with step size
if (cvalue !== null && cvalue % stepSize === 0) {
onChange(cvalue)
}
}, [clientX])
// Calculate slider value without taking into account the step size
// if the mouse's x position on the slider.
function calculateValue(clientX: number): number | null {
if (!sliderRect ||
clientX < sliderRect.left ||
clientX > sliderRect.right) {
return null;
}
const result = (clientX - sliderRect.left) * 100 / sliderRect.width
// So that the value can be 100
return result > 99 ? 100 : Math.round(result)
}
function handleMouseDown() {
document.addEventListener('mouseup', handleMouseUp)
document.addEventListener('mousemove', handleMouseMove)
}
function handleMouseUp() {
document.removeEventListener('mouseup', handleMouseUp)
document.removeEventListener('mousemove', handleMouseMove)
}
function handleMouseMove(event: MouseEvent) {
setClientX(event.clientX)
}
return (
<div
id="zoomContSlider"
style={{
width: `${sliderWidth}px`,
height: '6px'
}}
className={styles.slider}
>
{/* Tool tip */}
{toolTipVisibility && <div
style={{
left: `${calculateToolTipPosX(value, sliderWidth)}%`,
backgroundColor: sliderStyle?.tooltipColor,
color: sliderStyle?.tooltipTextColor
}}
className={styles.tooltip}
>
{value}
</div>}
{/* Slider track */}
<div
style={{backgroundColor: sliderStyle?.trackColor}}
className={styles.track}
></div>
{/* Slider value track */}
<div
style={{ width: `${value}%`, backgroundColor: sliderStyle?.valueTrackColor }}
className={styles.valueTrack}
></div>
{/* Point buttons */}
<div className={styles.pointButtons}>
{[25, 50, 75, 100].map(i => (
<div
key={i} onClick={() => onChange(i)}
style={{
left: `${i - (500/sliderWidth)}%`,
backgroundColor: sliderStyle?.valueTrackColor
}}
></div>
))}
</div>
{/* Slider thumb */}
<div
style={{
left: `${calculateThumbPosX(value, sliderWidth)}%`,
width: `${THUMB_SIZE}px`,
height: `${THUMB_SIZE}px`,
backgroundColor: sliderStyle?.thumbColor
}}
className={styles.thumb}
onMouseDown={handleMouseDown}
></div>
</div>
)
}
Here are the component's dropdown menu codes. You can check the comments for more details:
// Types
export interface ISelectBox {
value: number
options: number[]
selectBoxStyle?: ISelectBoxStyle
onChange: (value: number) => void
}
export type ISelectBoxStyle = {
backgroundColor?: string;
borderColor?: string
textColor?: string
downArrowColor?: string
optionBackgroundColor?: string
optionTextColor?: string
}
export type MenuOpeningDirection = 'up' | 'down'
import { MouseEvent, useRef, useState } from "react"
import { ISelectBox, MenuOpeningDirection } from "./index.types"
import styles from './style.module.css'
export default function SelectBox(
{ value, options, selectBoxStyle, onChange } : ISelectBox)
{
// For using input element's select(), blur() functions
const inputRef = useRef<HTMLInputElement>(null)
const [menuIsOpen, setMenuIsOpen] = useState<boolean>(false)
const [menuOpeningDirection, setMenuOpeningDirection] = useState<MenuOpeningDirection>('down')
// When clicked outside of selectbox close options and unfocus input
function onClickDocument() {
setMenuIsOpen(false)
document.removeEventListener('click', onClickDocument)
inputRef.current?.blur()
}
// Opens options downwards if it's not overflowing otherwise opens upwards
// Then focus and select input element for number entry.
function handleSelectBoxOnClick(event: MouseEvent) {
event.stopPropagation()
const docHeight = document.documentElement.clientHeight;
const mouseY = event.clientY;
const difference = docHeight - mouseY;
// Options container height = 200px
if (difference < 200) {
setMenuOpeningDirection('up')
} else {
setMenuOpeningDirection('down')
}
setMenuIsOpen((prev) => !prev)
inputRef.current?.select()
document.addEventListener('click', onClickDocument)
}
// The given zoom percentage can't be higher than highest number of option list.
function handleInputOnChange(event: React.ChangeEvent<HTMLInputElement>) {
let val = parseInt(event.target.value)
const maxInOptions = Math.max(...options)
val = isNaN(val) ? 100 : val;
if (val > maxInOptions) {
val = maxInOptions;
}
onChange(val)
}
function handleInputKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (event.key === 'Enter') {
setMenuIsOpen(false)
inputRef.current?.blur()
}
}
function handleOptionSelect(option: number) {
onChange(option)
}
return (
<div
style={{
color: selectBoxStyle?.textColor,
borderColor: selectBoxStyle?.borderColor,
backgroundColor: selectBoxStyle?.backgroundColor
}}
className={styles.main}
onClick={handleSelectBoxOnClick}
>
<input
ref={inputRef}
type="text"
value={value}
className={styles.input}
style={{color: selectBoxStyle?.textColor}}
onChange={handleInputOnChange}
onKeyDown={handleInputKeyDown}
/>
{menuIsOpen && <div className={styles.cursor}></div>}
<div className={styles.triangle} style={{borderTopColor: selectBoxStyle?.downArrowColor}}></div>
<div
className={styles.optionContainer}
style={{
display: menuIsOpen ? 'block' : 'none',
position: 'absolute',
top: menuOpeningDirection === 'down' ? '100%' : 'unset',
bottom: menuOpeningDirection === 'up' ? '100%' : 'unset',
width: '100%',
zIndex: '999'
}}
>
{options.map(o => (
<div
key={o}
className={styles.option}
style={{
color: selectBoxStyle?.optionTextColor,
backgroundColor: selectBoxStyle?.optionBackgroundColor,
}}
onClick={() => handleOptionSelect(o)}
>
{o}
</div>
))}
</div>
</div>
)
}
You can use the component on your react, next projects: https://www.npmjs.com/package/react-zoom-controller
You can see the source code of the component and an example project in my GitHub repository: https://github.com/canerdemirci/react-zoom-controller/
Subscribe to my newsletter
Read articles from caner demirci directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
caner demirci
caner demirci
Programming is fun. It is nice to spend time with it and not realize how time passes, learning new things... I hope one day I will make awesome things with it.