I'm trying to make it so that a box would expand (in width and height) and transition from its origin to the center of a screen upon being clicked. Here's what I have so far:
I'm running into two problems here -- when I click on the box, the DOM automatically shifts, because the clicked element has its position changed to 'absolute'
. The other problem is that the box doesn't transition from its origin, it transitions from the bottom right corner (it also doesn't return to its position from the center of the screen, when make inactive
is clicked).
What am I doing wrong here?
import React from "react";
import styled from "styled-components";
import "./styles.css";
export default function App() {
const [clickedBox, setClickedBox] = React.useState(undefined);
const handleClick = React.useCallback((index) => () => {
console.log(index);
setClickedBox(index);
});
return (
<Container>
{Array.from({ length: 5 }, (_, index) => (
<Box
key={index}
active={clickedBox === index}
onClick={handleClick(index)}
>
box {index}
{clickedBox === index && (
<div>
<button
onClick={(e) => {
e.stopPropagation();
handleClick(undefined)();
}}
>
make inactive
</button>
</div>
)}
</Box>
))}
</Container>
);
}
const Container = styled.div`
display: flex;
flex-wrap: wrap;
align-items: center;
height: 100vh;
`;
const Box = styled.div`
flex: 1 0 32%;
padding: 0.5rem;
cursor: pointer;
margin: 1rem;
border: 1px solid red;
transition: 2s;
background-color: white;
${({ active }) => `
${
active
? `
position: absolute;
width: 50vw;
height: 50vh;
background-color: tomato;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`
: ""
}
`}
`;
It works as follows: The overlay resting state is set to the right of the element using a margin (while the left is positioned to the left of the element). Upon hover, we set the margin to 0 without an animation. This allows the left animation to occur from the left side of the element.
Turns out, the rectangle in the back is an absolutely positioned ::after pseudo-element. Initial ::after styles. A positive offset goes inwards from the parent’s padding limit, while a negative one goes outwards. On :hover, its offsets are overridden and, combined with the transition, we get the expanding box effect.
It also works when the expanding (pseudo-) element is responsive, with no fixed dimensions and, at the same time, the amount by which it expands is fixed (a rem value). It also works for expanding in more than two directions ( top, bottom and left in this particular case). There are however a couple of caveats we need to be aware of.
We’ve all been there. You’ve got an element you want to be able to collapse and expand smoothly using CSS transitions, but its expanded size needs to be content-dependent. You’ve set transition: height 0.2s ease-out. You’ve created a collapsed CSS class that applies height: 0. You try it out, and… the height doesn’t transition.
The transition effect will start when the specified CSS property (width) changes value. Now, let us specify a new value for the width property when a user mouses over the <div> element:
Wery unlikely you can achieve that with plain css
. And for sure impossible to achieve a versatile solution.
div
from the layout is almost impossible to avoid screwing up the layout (even if you can, there will always be some edge case).relative
to a fixed
position.With the current css
standard is impossible to perform these things.
The solution is to do some javascript
magic. Since you are using React
i developed you a solution using react-spring
(an animation framework). Here you have a wrapping component that will do what you want:
The complete SandBox
import React, { useEffect, useRef, useState } from "react";
import { useSpring, animated } from "react-spring";
export default function Popping(props) {
const cont = useRef(null);
const [oriSize, setOriSize] = useState(null);
const [finalSize, setFinalSize] = useState(null);
useEffect(() => {
if (props.open && cont.current) {
const b = cont.current.getBoundingClientRect();
setOriSize({
diz: 0,
opacity: 0,
top: b.top,
left: b.left,
width: b.width,
height: b.height
});
const w = window.innerWidth,
h = window.innerHeight;
setFinalSize({
diz: 1,
opacity: 1,
top: h * 0.25,
left: w * 0.25,
width: w * 0.5,
height: h * 0.5
});
}
}, [props.open]);
const styles = useSpring({
from: props.open ? oriSize : finalSize,
to: props.open ? finalSize : oriSize,
config: { duration: 300 }
});
return (
<>
<animated.div
style={{
background: "orange",
position: "fixed",
display:
styles.diz?.interpolate((d) => (d === 0 ? "none" : "initial")) ||
"none",
...styles
}}
>
{props.popup}
</animated.div>
<div ref={cont} style={{ border: "2px solid green" }}>
{props.children}
</div>
</>
);
}
Note: This code uses two <div>
, one to wrap your content, and the second one is always fixed
but hidden. When you toggle the popup visibility, the wrapping div gets measured (we obtain its size and position on the screen) and the fixed
div is animated from that position to its final position. You can achieve the illusion you are looking for by rendering the same content in both <div>
, but there is always the risk of minor misalignment.
The idea is similar to what newbie did in their post but without any extra libraries. I might have done some things a bit non-standard to avoid using any libraries.
CodeSandbox
import React from "react";
import { StyledBox } from "./App.styles";
export const Box = (props) => {
const boxRef = React.useRef(null);
const { index, active, handleClick } = props;
const handleBoxClick = () => {
handleClick(index);
};
React.useEffect(() => {
const b = boxRef.current;
const a = b.querySelector(".active-class");
a.style.left = b.offsetLeft + "px";
a.style.top = b.offsetTop + "px";
a.style.width = b.offsetWidth + "px";
a.style.height = b.offsetHeight + "px";
});
return (
<StyledBox active={active} onClick={handleBoxClick} ref={boxRef}>
box {index}
<div className="active-class">
box {index}
<div>
<button
onClick={(e) => {
e.stopPropagation();
handleClick(undefined);
}}
>
make inactive
</button>
</div>
</div>
</StyledBox>
);
};
import styled from "styled-components";
export const StyledContainer = styled.div`
display: flex;
flex-wrap: wrap;
align-items: center;
height: 100vh;
`;
export const StyledBox = styled.div`
flex: 1 0 32%;
padding: 0.5rem;
cursor: pointer;
margin: 1rem;
border: 1px solid red;
background-color: white;
.active-class {
position: absolute;
transition: 0.3s all ease-in;
background-color: tomato;
z-index: -1;
${({ active }) =>
active
? `
width: 50vw !important;
height: 50vh !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%);
z-index: 1;
opacity: 1;
`
: `
z-index: -1;
transform: translate(0, 0);
opacity: 0;
`}
}
`;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With