React Resize Bounding is a simple, highly customizable React component that allows you to intuitively resize nested content using draggable border panels.
Interactive Grid (Example):
Installation
npm i react-resize-bounding
# or
yarn add react-resize-bounding
Usage
// @filename: MyComponent.tsx (.js)
import { useState } from "react";
import ResizeBounding from "react-resize-bounding";
export default function App() {
const [width, setWidth] = useState(320);
const [height, setHeight] = useState(480);
return (
<ResizeBounding
width={width}
height={height}
directions="hv"
updateWidth={(width) => setWidth(width)}
updateHeight={(height) => setHeight(height)}
style={{ border: "1px solid gray" }}
options={{ knob: { show: true } }}
// KNOB INNER CONTENT START
knob={<div className="some-icon"></div>}
// KNOB INNER CONTENT END
>
{/* CONTENT START */}
<div style={{ width: "100%", height: "100%" }}>My Container</div>
{/* CONTENT END */}
</ResizeBounding>
);
}
property | type | default value | description | |
---|---|---|---|---|
directions
|
string |
'' |
||
The literal 'hv' specifies which boundaries should be
enabled for resizing.The order of the characters is not significant. 'hv' is equivalent to 'tblr'
|
||||
value | description | |||
't' | top | |||
'r' | right | |||
'b' | bottom | |||
'l' | left | |||
'h' | horizontal alias, equivalent to 'lr' |
|||
'v' | vertical alias, equivalent to 'tb' |
|||
disabled
|
boolean |
false |
Disable border selection | |
width
|
number | undefined |
undefined |
Set current container width | |
minWidth
|
number | undefined |
0 |
Minimum value of the width resizing range | |
maxWidth
|
number | undefined |
undefined |
Maximum resizing range value. undefiend
Equivalent to Number.POSITIVE_INFINITY
|
|
height
|
number | undefined |
undefined |
Set current container height | |
minHeight
|
number | undefined |
0 |
Minimum height resizing range value | |
maxHeight
|
number | undefined |
undefined |
The maximum value of the height resizing range.
Equivalent to Number.POSITIVE_INFINITY
|
|
children
|
ReactNode |
undefined |
default slot: Content | |
knob
|
ReactNode |
undefined |
slot: Knob inner content (icon) | |
additional options | ||||
property | type | value | ||
options
|
Partial <Options>
|
|||
options.prefix
|
||||
description | Overrides the default class names prefix | |||
type | string |
|||
default value |
'resize-bounding-'
|
|||
options.width
|
||||
description | Set width of splitter in pixels | |||
type | number |
|||
default value |
4
|
|||
options.activeAreaWidth
|
||||
description | Sets the width of the active space within which the border (splitter) selection will be activated | |||
type | number | undefined |
|||
default value |
undefined
|
|||
options.position
|
||||
description | Determines the positioning of the splitter relative to the container boundaries | |||
type | SplitterPosition |
|||
default value |
'central'
|
|||
values: | ||||
'central'
|
||||
'internal'
|
||||
'external'
|
||||
options.touchActions
|
||||
description | Enable touch actions | |||
type | boolean |
|||
default value | true |
|||
options.addStateClasses
|
||||
description | Adds state classes to a pane element (.normal, .selected, .pressed) | |||
type | boolean |
|||
default value | false |
|||
options.knob.show
|
||||
description | Render the knob | |||
type | boolean |
|||
default value | false |
|||
options.knob.normalHidden
|
||||
description | Render the knob only when focusing or pressing on the splitter | |||
type | boolean |
|||
default value | false |
|||
options.cursor.vertical
|
||||
description | Cursor style for horizontal bounding during Focus and Resize | |||
type | CSSStyleDeclaration["cursor"] |
|||
default value | 'row-resize' |
|||
options.cursor.horizontal
|
||||
description | Cursor style for vertical bounding during Focus and Resize | |||
type | CSSStyleDeclaration["cursor"] |
|||
default value | 'col-resize' |
|||
styles
|
IStyles
|
|||
styles.container
|
||||
description |
Describes custom styles the container element. container is the
element directly in which the user content is located, forwarded
through <slot/>.
|
|||
type |
IStyle
|
|||
styles.pane
|
||||
description |
Describes custom styles the pane element. The pane element is a
container responsible for positioning the splitter. Therefore, treat
this component as an empty container, since you may only need to style
it in very rare cases.
Pane receives normal , focused and
pressed classes
|
|||
type |
IStyle
|
|||
styles.splitter
|
||||
description |
Describes custom styles the splitter element. splitter is an element
that displays a selected border line
|
|||
type |
IStyle
|
|||
styles.splitterContainer
|
||||
description |
Describes custom styles the splitterContainer element. splitterContainer is empty element used to rotating the knob
|
|||
type |
IStyle
|
|||
styles.knob
|
||||
description |
Describes custom styles the knob element. Knob is a decorative element
located on top of the splitter. Convenient to use with touch actions,
as it increases the touch area of the splitter by its own size and
has a positive effect on user experience
|
|||
type |
IStyle
|
property | type | description | |
---|---|---|---|
updateWidth
|
(width: number) => void |
Emitted every time a container width is updated | |
updateHeight
|
(height: number) => void |
Emitted every time a container height is updated | |
dragStart
|
(direction: PaneDirections) => void |
Emitted when resizing starts. The callback function accepts an
argument of current direction
|
|
dragMove
|
(direction: PaneDirections) => void |
Emitted when resizing. The callback function accepts an argument of
current direction
|
|
dragEnd
|
(direction: PaneDirections) => void |
Emitted when resizing ends. The callback function accepts an argument
of current direction
|
|
focus
|
({state: boolean, direction: PaneDirections}) => void |
Emitted when focusing on a specific boundary pane |
Overriding:
// @filename: MyResizeBoundingComponent.tsx (.js)
import ResizeBoundingComponent, { type Props } from "react-resize-bounding";
const ResizeBounding = (props: Props) => {
return (
<ResizeBoundingComponent
{...props}
options={{
knob: {
show: true,
},
}}
>
{props.children}
</ResizeBoundingComponent>
);
};
export default ResizeBounding;
Touch Area To increase the touch area, set the value to
options.activeAreaWidth
or use increased height of theknob
Default value is undefined
States styling:
By default, to style the active state (both .focused
or .pressed
), the .active
class is used;
So the style definition looks like this:
const styles = {
// Active (focused/pressed) state:
splitter: {
[`.${globalClassNames(prefix).pane}.active &`]: {
background: "cornflowerblue",
},
},
knob: {
[`.${globalClassNames(prefix).pane}.active &`]: {
background: "cornflowerblue",
},
},
};
To separately configure the focused state or the pressed state of a splitter/knob, use the included :options="{ addStateClasses: true }"
flag and the generated state classes:
const styles = {
splitter: {
// Focused state:
[`.${prefix}-pane.focused &`]: {
backgroundColor: "blue",
},
// Pressed state:
[`.${prefix}-pane.pressed &`]: {
backgroundColor: "red",
},
},
knob: {
// Focused state:
[`.${prefix}-pane.focused &`]: {
backgroundColor: "blue",
},
// Pressed state:
[`.${prefix}-pane.pressed &`]: {
backgroundColor: "red",
},
},
};
Using css
(preprocessors)
Use the included :options="{ addStateClasses: true }"
flag to style the .selected
and .pressed
states separately.
// @filename: MyResizeBounding.tsx (.js)
import { useState } from "react";
import ResizeBounding from "react-resize-bounding";
export default function App() {
const [width, setWidth] = useState(320);
const [height, setHeight] = useState(480);
return (
<ResizeBounding
width={width}
height={height}
directions="hv"
updateWidth={(width) => setWidth(width)}
updateHeight={(height) => setHeight(height)}
style={{ border: "1px solid gray" }}
options={{ addStateClasses: true, knob: { show: true } }}
// KNOB INNER CONTENT START
knob={<div className="some-icon"></div>}
// KNOB INNER CONTENT END
>
{/* CONTENT START */}
<div style={{ width: "100%", height: "100%" }}>My Container</div>
{/* CONTENT END */}
</ResizeBounding>
);
}
// @filename: MyResizeBounding.scss
$prefix: "resize-bounding-";
.#{$prefix} {
&-container {
}
&-pane {
/* Normal state */
.#{$prefix}splitter {
&--container {
}
}
.#{$prefix}knob {
}
/* * * Default `options` settings * * */
/* Both selected and pressed states */
&.active {
.#{$prefix}splitter {
}
.#{$prefix}knob {
}
}
/* * * Separate states ({ addStateClasses: true }) * * */
/* Normal state */
&.normal {
.#{$prefix}splitter {
}
.#{$prefix}knob {
}
}
/* Focused state */
&.focused {
.#{$prefix}splitter {
}
.#{$prefix}knob {
}
}
/* Pressed state */
&.pressed {
.#{$prefix}splitter {
}
.#{$prefix}knob {
}
}
}
}
// @filename: MyResizeBoundingComponent.tsx (.js)
import ResizeBounding, { PREFIX } from "vue3-resize-bounding";
/* * * Default styles and classes * * */
const options = {
width: 4,
activeAreaWidth: undefined,
position: "central", // 'central' | 'internal' | 'external'
knob: {
show: true,
normalHidden: true,
},
cursor: {
horizontal: "col-resize",
},
touchActions: true,
};
// Below are all the default styles purely for demonstration purposes
// In reality, you can only override the necessary properties
const styles = (prefix: string): IStyles => ({
container: [
globalClassNames(prefix).container,
{ displayName: globalClassNames(prefix).container, position: "relative" },
],
pane: [
globalClassNames(prefix).pane,
{
displayName: globalClassNames(prefix).pane,
position: "absolute",
display: "block",
zIndex: 9999,
touchAction: "none",
},
],
splitter: [
globalClassNames(prefix).splitter,
{
displayName: globalClassNames(prefix).splitter,
position: "absolute",
zIndex: 9999,
transition: "background 125ms ease-out",
[`.${globalClassNames(prefix).pane}.active &`]: {
background: "cornflowerblue",
},
/*
Focused state:
[`.${globalClassNames(prefix).pane}.focused &`]: {},
Pressed state:
[`.${globalClassNames(prefix).pane}.pressed &`]: {}
*/
},
],
splitterContainer: [
globalClassNames(prefix).splitterContainer,
{
displayName: globalClassNames(prefix).splitterContainer,
position: "relative",
top: "50%",
left: "50%",
width: `0px`,
height: `0px`,
},
],
knob: [
globalClassNames(prefix).knob,
{
displayName: globalClassNames(prefix).knob,
position: "relative",
width: "64px",
height: "6px",
background: "gray",
borderRadius: "3px",
transform: "translate(-50%, -50%)",
transition: "background 125ms ease-out",
[`.${globalClassNames(prefix).pane}.active &`]: {
background: "cornflowerblue",
},
/*
Focused state:
[`.${globalClassNames(prefix).pane}.focused &`]: {},
Pressed state:
[`.${globalClassNames(prefix).pane}.pressed &`]: {}
*/
},
],
});
Mikhail Grebennikov - yamogoo
This project is licensed under the terms of the MIT license.