Using React and Next.js, we'll recreate Tailwind's Resizeable Panels component.
Tailwind's Resizeable Panels component makes it easy to showcase responsive components. You don't have to worry about setting specific breakpoints; you can simply drag the resize handle to show your components at any size (useful for component prototyping).
Here's what we'll be building (drag the handle to resize the panel):
To follow along, create a new React project using create-react-app
.
To build the component, we need three things:
The container is going to be a single div
.
To start off, we're going to give the container a width and a height (and a background color, so that it's visible).
We'll set the container's width
property to 100%
. If you want the container to have a fixed width, say 736px for example, then set the maxWidth
property to 736px
. That way, when you're on screens smaller than 736px, the container's width will scale down responsively instead of staying at 736px, which would be too large.
We can set the container's height
property to any value, so I'll choose 400px
.
Here's what the code looks like:
const ResizablePanels = () => {
return (
<div
style={{
width: "100%",
maxWidth: "736px",
height: "400px",
backgroundColor: "#6b7280",
}}
>
</div>
)
}
And here's our container:
The resizeable panel is going to be one div
that contains both the panel's content (e.g. a responsive component prototype) and the div
for the resize handle (which will be positioned absolutely, relative to the resizeable panel).
We'll make the panel's position
property relative
so that when we resize the panel, we don't have to write any additional code for moving the resize handle; the absolutely positioned resize handle will automatically shift so that it's always "attached" to the right side of the panel.
We can give the resizeable panel a height
of 100%
so that it fills up the container, and we can also give it a background color so that it's visible.
Since we're going to be changing the resizeable panel's width, let's give it a maxWidth
of 100%
so that it's never larger than its container.
For the width, the panel needs to fill up the container, while leaving space for the resize handle. Since the resize handle is positioned absolutely relative to the resizeable panel, we can give the panel a width
of 100%
and a paddingRight
of the resize handle's width to make sure that the container can fully contain both the resizeable panel and the resize handle.
Note: For the resizeable panel to fit in the container, we need to set box-sizing
to border-box
in index.css
:
/* index.css */
* {
box-sizing: border-box;
}
We can store the value of the resize handle's width in a variable named handleWidth
so that it's easy to reuse.
Here's what the code looks like:
const ResizablePanels = () => {
const handleWidth = 16;
return (
<div
style={{
width: "100%",
maxWidth: "736px",
height: "400px",
backgroundColor: "#6b7280",
}}
>
<div
style={{
position: "relative",
width: "100%",
maxWidth: "100%",
height: "100%",
paddingRight: `${handleWidth}px`,
backgroundColor: "#f1f2f4",
}}
>
</div>
</div>
)
}
And here's our container with our resizeable panel inside:
The resize handle is going to be a single div
.
We'll set the handle's position
property to absolute
then set it's top
and right
properties to 0
so that the handle remains "attached" to the resizeable panel.
The handle's height
will be 100%
and its width will be the handleWidth
variable that we defined earlier.
We'll give the handle a background color so that it's visible and also a left border to visually separate it from the resize handle.
We'll also set the handle's cursor
property to ew-resize
to indicate that the handle is draggable.
Here's what the code looks like:
const ResizablePanels = () => {
const handleWidth = 16;
return (
<div
style={{
width: "100%",
maxWidth: "736px",
height: "400px",
backgroundColor: "#6b7280",
}}
>
<div
style={{
position: "relative",
width: "100%",
maxWidth: "100%",
height: "100%",
paddingRight: `${handleWidth}px`,
backgroundColor: "#f1f2f4",
}}
>
<div
style={{
position: "absolute",
top: "0",
right: "0",
height: "100%",
width: `${handleWidth}px`,
backgroundColor: "#f3f4f6",
borderLeft: "1px solid #e5e7eb",
// borderBottom: "1px solid #e5e7eb",
cursor: "ew-resize",
}}
></div>
</div>
</div>
)
}
And here's our container with our resizeable panel and resize handle inside:
Now that we have the component set up, it's time to make it so that when you drag the resize handle, the resizeable panel gets resized.
To accomplish this resizing effect, we're going to create a useResize
hook (adapted from this StackOverflow answer).
The hook is going to accept four arguments:
containerRef
: a reference to the containerpanelRef
: a reference to the resizeable panelinitialWidth
: the resizeable panel's inital widthminWidth
: an optional argument for the resizeable panel's minimum width (will have a default value of 0)Inside the hook, we're going to create a variable called panelWidth
which we'll eventually set as the resizeable panel's width. Since we want the resizeable panel's width in the UI to be updated when the panelWidth
variable is updated, we'll create the panelWidth
variable using the useState
hook and give it an initial value of initialWidth
.
To help us handle the different stages of resizing, we're going to create three functions inside the hook: onResizeStart
, onResize
, and onResizeEnd
.
The hook will return the panelWidth
variable and the onResizeStart
function. We'll set the resizeable panel's width to the panelWidth
variable and we'll use the onResizeStart
function in the resize handle so that when the handle is clicked, onResizeStart
will be called and resizing can occur.
Here's what the code for the useResize
hook looks like so far:
const useResize = ({
containerRef,
panelRef,
initialWidth,
minWidth = 0,
}): => {
const [panelWidth, setPanelWidth] = useState(initialWidth);
// TODO
const onResizeStart = () => {};
// TODO
const onResize = (e) => {};
// TODO
const onResizeEnd = () => {};
return { panelWidth, onResizeStart };
}
onResizeStart
FunctionWhen resizing starts, we want to make sure that resizing occurs when the user drags their mouse and that resizing stops when the user releases their mouse. So, in our onResizeStart
function, we'll add event listeners that will call onResize
when dragging occurs (pointermove
event) and call onResizeEnd
when the mouse is released (pointerup
event).
We'll also set the resizeable panel's pointerEvents
and userSelect
properties to none
so that nothing in the panel gets selected while the panel is being resized.
In the component that we have so far, you'll notice that when you click the resize handle and drag to the left, the cursor changes from ew-resize
to the default cursor when you move your mouse from the panel to the resizeable panel. To fix this shift in cursors, we'll set the container's cursor to ew-resize
when resizing starts.
Here's what the code looks like:
const onResizeStart = () => {
if (panelRef.current) {
panelRef.current.style.pointerEvents = "none";
panelRef.current.style.userSelect = "none";
}
if (containerRef.current) {
containerRef.current.style.cursor = "ew-resize";
}
window.addEventListener("pointermove", onResize);
window.addEventListener("pointerup", onResizeEnd);
};
onResize
FunctionWhile resizing is happening, the newWidth
of the resizeable panel if going to be equal to the mouse's position – the leftmost part of the resizeable panel.
We'll only set the panel's width to the newWidth
if the newWidth
is greater than the panel's minWidth
.
Here's what the code looks like:
const onResize = (e) => {
if (containerRef.current) {
const bounds = containerRef.current.getBoundingClientRect();
const newWidth = e.clientX - bounds.left;
if (newWidth >= minWidth) {
setPanelWidth(newWidth);
} else {
setPanelWidth(minWidth);
}
}
};
onResizeEnd
FunctionWhen resizing ends, we want to remove the pointermove
and pointerup
event listeners that we added in our onResizeStart
function, since we're no longer resizing.
We'll also set the resizeable panel's pointerEvents
and userSelect
properties to auto
so that the panel's contents are selectable again, and we'll reset the container's cursor to auto
.
Here's what the code looks like:
const onResizeEnd = () => {
if (panelRef.current) {
panelRef.current.style.pointerEvents = "auto";
panelRef.current.style.userSelect = "auto";
}
if (containerRef.current) {
containerRef.current.style.cursor = "auto";
}
window.removeEventListener("pointermove", onResize);
window.removeEventListener("pointerup", onResizeEnd);
};
useResize
Hook in the Resizeable PanelHere's what the code for the useResize
hook looks like:
const useResize = ({
containerRef,
panelRef,
initialWidth,
minWidth = 0,
}): => {
const [panelWidth, setPanelWidth] = useState(initialWidth);
const onResizeStart = () => {
if (panelRef.current) {
panelRef.current.style.pointerEvents = "none";
panelRef.current.style.userSelect = "none";
}
if (containerRef.current) {
containerRef.current.classList.add("resizing");
containerRef.current.style.cursor = "ew-resize";
}
window.addEventListener("pointermove", onResize);
window.addEventListener("pointerup", onResizeEnd);
};
const onResize = (e) => {
if (containerRef.current) {
const bounds = containerRef.current.getBoundingClientRect();
const newWidth = e.clientX - bounds.left;
if (newWidth >= minWidth) {
setPanelWidth(newWidth);
} else {
setPanelWidth(minWidth);
}
}
};
const onResizeEnd = () => {
if (panelRef.current) {
panelRef.current.style.pointerEvents = "auto";
panelRef.current.style.userSelect = "auto";
}
if (containerRef.current) {
containerRef.current.classList.remove("resizing");
containerRef.current.style.cursor = "auto";
}
window.removeEventListener("pointermove", onResize);
window.removeEventListener("pointerup", onResizeEnd);
};
return { panelWidth, onResizeStart };
}
Here's what the updated code for the ResizeablePanels
component looks like:
const ResizablePanels = () => {
const containerRef = useRef(null);
const panelRef = useRef(null);
const handleWidth = 16;
const maxContainerWidth = 736;
const { panelWidth, onResizeStart } = useResize(
containerRef,
panelRef,
maxContainerWidth,
);
return (
<div
ref={containerRef}
style={{
width: "100%",
maxWidth: `${maxContainerWidth}px`,
height: "400px",
backgroundColor: "#6b7280",
}}
>
<div
ref={panelRef}
style={{
position: "relative",
width: `${panelWidth}px`,
maxWidth: "100%",
height: "100%",
paddingRight: `${handleWidth}px`,
backgroundColor: "#f1f2f4",
}}
>
<div
style={{
position: "absolute",
top: "0",
right: "0",
height: "100%",
width: `${handleWidth}px`,
backgroundColor: "#f3f4f6",
borderLeft: "1px solid #e5e7eb",
cursor: "ew-resize",
}}
onPointerDown={onResizeStart}
></div>
</div>
</div>
)
}
And here's the CodeSandbox our component with resize working:
Hopefully you learned something new by recreating Tailwind's Resizeable Panels component.
If you want to learn more, you could try to extend the component by:
Thanks for reading! If anything in this article was unclear or if you have any questions, you can reach me on Twitter @qildev.