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/