A draggable and zoomable grid system for placing items.
Report Bug
·
Request Feature
Table of Contents
# npm
npm install motion @radix-ui/react-slider
# yarn
yarn add motion @radix-ui/react-slider
# pnpm
pnpm add motion @radix-ui/react-slider
# bun
bun install motion @radix-ui/react-slider# npm
npm install dynamic-gridline
# yarn
yarn add dynamic-gridline
# pnpm
pnpm add dynamic-gridline
# bun
bun install dynamic-gridlineImport the components and start building your interactive grid:
import { Grid, GridItem } from 'dynamic-gridline'
function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<Grid>
<GridItem x={100} y={100}>
<div>Your content here</div>
</GridItem>
</Grid>
</div>
)
}
The Grid component is the main container that provides the interactive canvas with pan, zoom, and grid functionality.
import { Grid } from 'dynamic-gridline'
function InteractiveCanvas() {
return (
<Grid
config={{
width: 10000,
height: 8000,
gridCellSize: 50,
minZoom: 0.1,
maxZoom: 2,
gridColor: '#e0e0e0',
}}
>
{/* Your grid items go here */}
</Grid>
)
}
The GridItem component positions content within the grid coordinate system.
import { Grid, GridItem } from 'dynamic-gridline'
function ItemPlacement() {
return (
<Grid>
<GridItem x={0} y={0}>
<div style={{ background: 'red', text: 'white', padding: '10px' }}>(0,0)</div>
</GridItem>
<GridItem x={200} y={150}>
<div style={{ background: 'blue', text: 'white', padding: '10px' }}>(200,150)</div>
</GridItem>
<GridItem x={-150} y={-50}>
<div style={{ background: 'green', text: 'white', padding: '10px' }}>(-150, -50)</div>
</GridItem>
</Grid>
)
}
Control whether items scale with zoom using the disableScale property:
function ScalingDemo() {
return (
<Grid config={{ minZoom: 0.5, maxZoom: 2 }}>
{/* This circle will scale with zoom */}
<GridItem x={-100} y={0} disableScale={false}>
<div
style={{
width: 120,
height: 120,
color: 'white',
borderRadius: '50%',
background: 'blue',
}}
></div>
</GridItem>
{/* This circle maintains constant size */}
<GridItem x={100} y={0} disableScale={true}>
<div
style={{
width: 120,
height: 120,
color: 'white',
borderRadius: '50%',
background: 'red',
}}
></div>
</GridItem>
</Grid>
)
}Control layering of grid items with fixedZIndex:
function LayeringDemo() {
return (
<Grid>
<GridItem x={-50} y={-50} fixedZIndex={1}>
<div
style={{
width: 100,
height: 100,
background: 'rgba(255,0,0,0.7)',
color: 'white',
}}
>
Behind (z-index: 1)
</div>
</GridItem>
<GridItem x={0} y={0} fixedZIndex={10}>
<div
style={{
width: 100,
height: 100,
background: 'rgba(0,0,255,0.7)',
color: 'white',
}}
>
In Front (z-index: 10)
</div>
</GridItem>
</Grid>
)
}
Handle fast clicks and hold clicks on the grid:
function ClickHandling() {
const handleFastClick = ({ x, y }: { x: number; y: number }) => {
console.log('Fast click at:', x, y)
// Add logic for quick interactions
}
const handleHoldClick = ({ x, y }: { x: number; y: number }) => {
console.log('Hold click at:', x, y)
// Add logic for context menus or long-press actions
}
return (
<Grid
config={{
onFastClick: handleFastClick,
onHoldClick: handleHoldClick,
}}
>
<GridItem x={0} y={0}>
<div>Try clicking and holding on the grid!</div>
</GridItem>
</Grid>
)
}Track mouse position within the grid coordinate system:
function MouseTracking() {
const [mousePos, setMousePos] = useState({ x: 0, y: 0 })
const handleMouseMove = ({ x, y }: { x: number; y: number }) => {
setMousePos({ x, y })
}
return (
<div style={{ width: '100vw', height: '100vh' }}>
<p style={{ position: 'absolute', top: 0, left: 0 }}>
Mouse position: ({mousePos.x.toFixed(2)}, {mousePos.y.toFixed(2)})
</p>
<Grid config={{ onMouseMove: handleMouseMove }}>
<GridItem x={mousePos.x} y={mousePos.y}>
<div
style={{
width: 10,
height: 10,
background: 'red',
borderRadius: '50%',
}}
/>
</GridItem>
</Grid>
</div>
)
}Navigate the grid using arrow keys:
function KeyboardNavigation() {
return (
<Grid
config={{
panStep: 50, // Pixels to move per arrow key press
keyDisabled: false, // Enable keyboard controls
}}
>
<GridItem x={0} y={0}>
<div>Use arrow keys to pan around!</div>
</GridItem>
</Grid>
)
}Built-in zoom slider and wheel zoom functionality:
function ZoomDemo() {
return (
<Grid
config={{
minZoom: 0.25,
maxZoom: 4,
zoomSteps: 50,
wheelDisabled: false, // Enable mouse wheel zoom
}}
>
<GridItem x={0} y={0}>
<div>Use mouse wheel or slider to zoom!</div>
</GridItem>
</Grid>
)
}Customize the grid's visual appearance:
function CustomStyling() {
return (
<Grid
config={{
gridBackground: '#1b2845',
gridColor: '#4f88cb', // Custom grid line color
gridCellSize: 30, // Smaller grid cells
}}
>
<GridItem x={0} y={0}>
<div style={{ color: 'white' }}>Custom styled grid!</div>
</GridItem>
</Grid>
)
}
Replace the default zoom slider with your own component:
function CustomZoomSlider() {
const customSlider = (sliderProps: SliderProps) => (
<div
style={{
position: 'absolute',
top: 20,
right: 20,
background: 'white',
padding: '10px',
borderRadius: '8px',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
}}
>
<label>Zoom: {sliderProps.zoomValue.toFixed(2)}x</label>
<input
type="range"
min={Math.log10(sliderProps.minZoom)}
max={Math.log10(sliderProps.maxZoom)}
step={
(Math.log10(sliderProps.maxZoom) - Math.log10(sliderProps.minZoom)) /
(sliderProps.zoomSteps - 1)
}
value={Math.log10(sliderProps.zoomValue)}
onChange={(e) => sliderProps.handleZoom(10 ** parseFloat(e.target.value))}
/>
</div>
)
return (
<Grid config={{ customZoomSlider: customSlider }}>
<GridItem x={0} y={0}>
<div>Custom zoom control!</div>
</GridItem>
</Grid>
)
}
Disable specific interactions while keeping others active:
function SelectiveDisabling() {
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '100vw',
height: '100vh',
gap: '20px',
}}
>
<div style={{ width: '30vw', aspectRatio: '1/1' }}>
<Grid config={{ panDisabled: true, wheelDisabled: false }}>
<GridItem x={0} y={0}>
<div>Zoom only</div>
</GridItem>
</Grid>
</div>
{/* Wheel zoom disabled, pan still works */}
<div style={{ width: '30vw', aspectRatio: '1/1' }}>
<Grid config={{ panDisabled: false, wheelDisabled: true }}>
<GridItem x={0} y={0}>
<div>Pan only</div>
</GridItem>
</Grid>
</div>
{/* Everything disabled */}
<div style={{ width: '30vw', aspectRatio: '1/1' }}>
<Grid config={{ disabled: true }}>
<GridItem x={0} y={0}>
<div>Static view</div>
</GridItem>
</Grid>
</div>
</div>
)
}
Access grid methods using a ref:
import { useRef } from 'react'
import { Grid, GridRef } from 'dynamic-gridline'
function GridWithRef() {
const gridRef = useRef<GridRef>(null)
const focusGrid = () => {
gridRef.current?.focusGrid()
}
return (
<div>
<button onClick={focusGrid}>Focus Grid (for keyboard controls)</button>
<Grid ref={gridRef}>
<GridItem x={0} y={0}>
<div>Grid with ref access</div>
</GridItem>
</Grid>
</div>
)
}The Grid component accepts a config prop with the following options:
| Property | Type | Default | Description |
|---|---|---|---|
width |
number |
window.innerWidth * 10 |
Total width of the grid canvas in pixels |
height |
number |
window.innerHeight * 10 |
Total height of the grid canvas in pixels |
gridCellSize |
number |
20 |
Size of each grid cell in pixels |
gridBackground |
string |
'transparent' |
Background color of the grid container |
gridColor |
string |
'oklch(70.7% 0.022 261.325)' |
Color of the grid lines |
minZoom |
number |
0.1 |
Minimum zoom level (0.1 = 10%) |
maxZoom |
number |
1 |
Maximum zoom level (1 = 100%) |
zoomSteps |
number |
100 |
Number of steps in the zoom slider |
panStep |
number |
10 |
Pixels to move when using keyboard navigation |
disabled |
boolean |
false |
Disable all interactions (pan, zoom, keyboard) |
panDisabled |
boolean |
false |
Disable panning (mouse drag and touch) |
wheelDisabled |
boolean |
false |
Disable mouse wheel zooming |
keyDisabled |
boolean |
false |
Disable keyboard navigation |
customZoomSlider |
((props: SliderProps) => ReactNode) | null |
null |
Custom zoom slider component |
onFastClick |
({ x, y }: { x: number; y: number }) => void |
undefined |
Callback for quick clicks on the grid |
onHoldClick |
({ x, y }: { x: number; y: number }) => void |
undefined |
Callback for long presses on the grid |
onMouseMove |
({ x, y }: { x: number; y: number }) => void |
undefined |
Callback for mouse movement over the grid |
| Property | Type | Default | Description |
|---|---|---|---|
x |
number |
Required | X coordinate in grid space |
y |
number |
Required | Y coordinate in grid space |
disableScale |
boolean |
false |
Prevent item from scaling with zoom |
fixedZIndex |
number |
undefined |
Fixed z-index for layering control |
children |
ReactNode |
Required | Content to render in the grid item |
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature) - Commit your Changes (
git commit -m 'Add some AmazingFeature') - Push to the Branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Distributed under the MIT License. See LICENSE for more information.
Omar Hassan - @omar_elfat76510 - [email protected]
Project Link: https://github.com/elfatairy/dynamic-gridline
Portfolio: https://omarhassan.net



