I'm trying to build an responsive list layout with an expandable card, the goal would be this:
I build the card, on a separated component called MaterialCard and tried to organize all the cards on a component called MaterialCardList.
MaterialCard:
This component expands horizontally through max-width and isExpanded boolean expands it vertically adding content to the card.
export const MaterialCard: React.FC<IMaterialCard> = ({ material }) => {
const [isExpanded, setIsExpanded] = useState<boolean>(false)
return (
<CustomContainer onClick={() => setIsExpanded(!isExpanded)} style={isExpanded ? { maxWidth: 635 } : {maxWidth: 310 }}>
...
MaterialCardList: Here is where all MaterialCard are rendered, and the main problem as I see
export const MaterialCardList: React.FC = () => {
return (
<Organizer>
{!!materials.length && materials.map((material) => (
<MaterialCard key={material.id} material={material} />
))}
</Organizer>
)
}
Organizer:
This is the styled-component of the MaterialCardList
export const Organizer = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(auto, 310px));
grid-column-gap: 16px;
grid-row-gap: 16px;
`;
The result was something like this:
I understand that minmax on grid does not let my card open the width, but i don't know where to go from here to achieve the layout, there any easier way to do this without grid? What next step to achieve the goal ?
Since the goal is a 2 dimensional layout, I think grid
is still a suitable approach for it.
The following is a basic example that defines a fixed number of columns based on responsive value. It specifies an explicit position for the expanded
card ,and let other cards fill the gap by auto placement.
Live of the example: stackblitz
The MaterialCardList
component could use media queries to define the number of columns needed here, or it could be fixed depending on the use case.
const Organizer = styled.section`
display: grid;
grid-auto-rows: 150px;
grid-auto-flow: row dense;
gap: 24px;
flex: 1;
max-width: 900px;
grid-template-columns: repeat(2, 1fr);
@media screen and (min-width: 550px) {
grid-template-columns: repeat(3, 1fr);
}
@media screen and (min-width: 750px) {
grid-template-columns: repeat(4, 1fr);
}
`;
const MaterialCardList = ({ materialData }: IMaterialCardList) => {
const [expanded, setExpanded] = useState<number | null>(null);
return (
<Organizer>
{materialData.map((item, index) => (
<MaterialCard
material={item}
key={item}
index={index}
isExpanded={index === expanded}
onToggleExpand={() =>
setExpanded((prev) => (prev === null ? index : null))
}
/>
))}
</Organizer>
);
};
The MaterialCard
component specifies an explicit position for the expanded
card based on its index
and the columns of the grid.
While media queries are used in StyledCard
, they are just for getting the column number of the grid, and can be omitted if the component can get responsive props for this, either from a helper hook or an UI library used in addition to styled-components
.
interface IStyledCard {
readonly isExpanded?: boolean;
readonly index: number;
}
const StyledCard = styled.div<IStyledCard>`
display: flex;
justify-content: center;
align-items: center;
background-color: #fff;
color: #000;
font-size: large;
border-radius: 20px;
outline: 5px solid #000;
box-shadow: 10px 10px 1px 1px darkgray;
cursor: pointer;
grid-column-start: ${({ isExpanded, index }) =>
!isExpanded ? "auto" : (index + 1) % 2 === 0 ? "1" : `${(index % 2) + 1}`};
grid-column-end: ${({ isExpanded }) => (isExpanded ? "span 2" : "span 1")};
grid-row-start: ${({ isExpanded, index }) =>
isExpanded ? `${Math.floor(index / 2 + 1)}` : "auto"};
grid-row-end: ${({ isExpanded }) => (isExpanded ? "span 2" : "span 1")};
@media screen and (min-width: 550px) {
grid-column-start: ${({ isExpanded, index }) =>
!isExpanded
? "auto"
: (index + 1) % 3 === 0
? "2"
: `${(index % 3) + 1}`};
grid-row-start: ${({ isExpanded, index }) =>
isExpanded ? `${Math.floor(index / 3 + 1)}` : "auto"};
}
@media screen and (min-width: 750px) {
grid-column-start: ${({ isExpanded, index }) =>
!isExpanded
? "auto"
: (index + 1) % 4 === 0
? "3"
: `${(index % 4) + 1}`};
grid-row-start: ${({ isExpanded, index }) =>
isExpanded ? `${Math.floor(index / 4 + 1)}` : "auto"};
}
`;
interface IMaterialCard {
readonly material: string;
readonly index: number;
readonly isExpanded: boolean;
readonly onToggleExpand: () => void;
}
const MaterialCard = ({
material,
index,
isExpanded,
onToggleExpand,
}: IMaterialCard) => {
return (
<StyledCard isExpanded={isExpanded} index={index} onClick={onToggleExpand}>
{material}
</StyledCard>
);
};
The explicit position for expanded
item is only basic here, which could use a lot of adjustments to be more natural, and perhaps further customization could also be done to support multiple expanded
items if needed.
If the layout also need to be animated, perhaps the easiest way could be using a library with built-in support for layout animations such as framer-motion.
Here is a basic example with animation: stackblitz