Skip to content

Instantly share code, notes, and snippets.

@netgfx
Last active February 25, 2024 00:13
Show Gist options
  • Save netgfx/a4d6f6f6ec07d1b1ffa9fdad50e043b4 to your computer and use it in GitHub Desktop.
Save netgfx/a4d6f6f6ec07d1b1ffa9fdad50e043b4 to your computer and use it in GitHub Desktop.
Framer Masonry (pinterest layout)
import { useEffect, useState } from "react"
import { motion, Variants } from "framer-motion"
import { addPropertyControls, ControlType } from "framer"
import React from "react"
// Welcome to Code in Framer
// Get Started: https://www.framer.com/docs/guides/
export function useMediaQuery(query) {
const [matches, setMatches] = useState(false)
useEffect(() => {
const media = window.matchMedia(query)
if (media.matches !== matches) {
setMatches(media.matches)
}
const listener = () => {
setMatches(media.matches)
}
media.addListener(listener)
return () => media.removeListener(listener)
}, [matches, query])
return matches
}
export const useIsSmall = (mediaQuerySmall) =>
useMediaQuery("(min-width: " + mediaQuerySmall + "px)")
export const useIsBig = (mediaQueryBig) =>
useMediaQuery("(min-width: " + mediaQueryBig + "px)")
export const useIsMedium = (mediaQueryMedium) =>
useMediaQuery("(min-width: " + mediaQueryMedium + "px)")
/**
* These annotations control how your component sizes
* Learn more: https://www.framer.com/docs/guides/auto-sizing
*
* @framerSupportedLayoutWidth any
* @framerSupportedLayoutHeight any
*/
export default function MasonryLayout(props) {
const {
animate,
gutter,
vgutter,
columns,
components,
mediaQuerySmall,
mediaQueryMedium,
mediaQueryBig,
columnsSmall,
columnsMedium,
columnsBig,
style,
} = props
// media queries
const [active, setActive] = useState(false)
const [columnsNum, setColumnsNum] = useState(props.columnsBig)
const isSmall = useIsSmall(props.mediaQuerySmall)
const isMedium = useIsMedium(props.mediaQueryMedium)
const isBig = useIsBig(props.mediaQueryBig)
useEffect(() => {
if (isBig) {
setColumnsNum(props.columnsBig)
} else if (isMedium) {
setColumnsNum(props.columnsMedium)
} else {
setColumnsNum(props.columnsSmall)
}
}, [isSmall, isMedium, isBig])
const cardVariants: Variants = {
offscreen: {
scale: props.animate == true ? 0.5 : 1,
},
onscreen: {
scale: 1,
transition: {
type: "spring",
bounce: 0.4,
duration: 0.8,
},
},
}
const getColumns = () => {
if (isBig) {
return props.columnsBig
} else if (isMedium) {
return props.columnsMedium
} else if (isSmall) {
return props.columnsSmall
} else {
return 4
}
}
return (
<motion.div style={{ ...style, ...containerStyle }}>
<style>{masonryStyle}</style>
<motion.div
className="masonry-with-flex"
style={{ columnGap: gutter, columnCount: getColumns() }}
>
{React.Children.map(components, (child, index) => {
return (
<motion.figure
layout={props.animate}
variants={cardVariants}
initial={
props.animate == true ? "offscreen" : false
}
whileInView={
props.animate == true ? "onscreen" : "offscreen"
}
viewport={
props.animate == true
? { once: true, amount: 0.8 }
: {}
}
style={{ ...figureStyle, marginBottom: vgutter }}
>
{React.cloneElement(child, {
width: "100%",
key: index,
// Additional Props
})}
</motion.figure>
)
})}
</motion.div>
</motion.div>
)
}
MasonryLayout.defaultProps = {
gutter: 10,
columns: 4,
}
addPropertyControls(MasonryLayout, {
components: {
title: "Components",
type: ControlType.Array,
control: {
type: ControlType.ComponentInstance,
},
maxCount: 16,
},
animate: {
title: "Animate",
type: ControlType.Boolean,
defaultValue: true,
},
gutter: {
title: "Gap",
type: ControlType.Number,
defaultValue: 10,
},
vgutter: {
title: "Vertical Gap",
type: ControlType.Number,
defaultValue: 10,
},
mediaQuerySmall: {
title: "Media Query Small",
type: ControlType.Number,
defaultValue: 390,
},
mediaQueryMedium: {
title: "Media Query Medium",
type: ControlType.Number,
defaultValue: 1024,
},
mediaQueryBig: {
title: "Media Query Big",
type: ControlType.Number,
defaultValue: 1400,
},
columnsSmall: {
title: "Media Query Small",
type: ControlType.Number,
defaultValue: 1,
},
columnsMedium: {
title: "Media Query Medium",
type: ControlType.Number,
defaultValue: 3,
},
columnsBig: {
title: "Media Query Big",
type: ControlType.Number,
defaultValue: 5,
},
})
const containerStyle = {
padding: 8,
width: "100%",
minWidth: 390,
height: "100%",
boxSizing: "border-box",
}
const figureStyle: React.CSSProperties = {
margin: 0,
display: "grid",
gridTemplateRows: "1fr auto",
marginBottom: "10px",
breakInside: "avoid",
}
const masonryStyle = `
.masonry-with-flex {
column-count: 4;
column-gap: 10px;
}
figure > div {
max-width: 100%;
display: block;
}
figure > img {
grid-row: 1 / -1;
grid-column: 1;
}
/*.masonry-with-flex > * {
margin: 0 1rem 1rem 0;
text-align: center;
}*/
`
@netgfx
Copy link
Author

netgfx commented Dec 12, 2022

do you have a demo of this by any chance

Absolutely, I have a whole article :)
https://medium.com/7l-international/build-a-layout-system-with-framer-e8ea577ff608

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment