Skip to content

Instantly share code, notes, and snippets.

@Blankeos
Created May 21, 2025 21:42
Show Gist options
  • Select an option

  • Save Blankeos/6eba1116f4533525042e60ef7638e83e to your computer and use it in GitHub Desktop.

Select an option

Save Blankeos/6eba1116f4533525042e60ef7638e83e to your computer and use it in GitHub Desktop.
masonry.svelte
<!-- Masonry.svelte -->
<!-- @component
I got this component from a library called svelte-mason. Just upgraded it to Svelte 5.
-->
<script lang="ts" generics="Item">
import type { Snippet } from 'svelte';
type Props = {
animate?: boolean;
calcCols?: (masonryWidth: number, minColWidth: number, gap: number) => number;
class?: string;
columnClass?: string;
duration?: number;
gap?: number;
getId?: (item: Item) => string | number;
idKey?: string;
items?: Item[];
masonryHeight?: number;
masonryWidth?: number;
maxColWidth?: number;
minColWidth?: number;
style?: string;
children?: Snippet<[{ idx: number; item: Item }]>;
};
let {
items = [],
animate = true,
calcCols = (masonryWidth: number, minColWidth: number, gap: number): number => {
return Math.min(items.length, Math.floor((masonryWidth + gap) / (minColWidth + gap)) || 1);
},
class: className = '',
columnClass = ``,
duration = 200,
gap = 20,
getId = (item: Item): string | number => {
if (typeof item === `number`) return item;
if (typeof item === `string`) return item;
return (item as Record<string, string | number>)[idKey as any]!;
},
idKey = `id`,
masonryHeight = $bindable(0),
masonryWidth = $bindable(0),
maxColWidth = 500,
minColWidth = 330,
style = ``,
children,
}: Props = $props();
$effect(() => {
if (maxColWidth < minColWidth) {
console.warn(`svelte-bricks: maxColWidth (${maxColWidth}) < minColWidth (${minColWidth}).`);
}
});
let nCols = $derived(calcCols(masonryWidth, minColWidth, gap));
let itemsToCols = $derived(
items.reduce(
(cols: [Item, number][][], item, idx) => {
cols[idx % cols.length]?.push([item, idx]);
return cols;
},
Array(nCols).fill(null).map(() => []) // prettier-ignore
)
);
</script>
<div
class="masonry {className}"
bind:clientWidth={masonryWidth}
bind:clientHeight={masonryHeight}
style="gap: {gap}px; {style}"
>
{#each itemsToCols as col, i (i)}
<div class="col {columnClass}" style="gap: {gap}px; max-width: {maxColWidth}px;">
{#if animate}
{#each col as [item, idx] (getId(item))}
<div
in:fade={{ delay: 100, duration }}
out:fade={{ delay: 0, duration }}
animate:flip={{ duration }}
>
{@render children?.({ idx: idx, item: item })}
<!-- <slot {idx} {item}>
<span>{item}</span>
</slot> -->
</div>
{/each}
{:else}
{#each col as [item, idx] (getId(item))}
{@render children?.({ idx: idx, item: item })}
{/each}
{/if}
</div>
{/each}
</div>
<style>
:where(div.masonry) {
display: flex;
justify-content: center;
overflow-wrap: anywhere;
box-sizing: border-box;
}
:where(div.masonry div.col) {
display: grid;
height: max-content;
width: 100%;
}
</style>
<!-- --------- USAGE --------- -->
<script>
let minColWidth = $state(250);
let maxColWidth = $state(800);
let gap = $state(10);
let width = $state(0);
let height = $state(0);
</script>
<Masonry
animate={false}
items={testimonials}
getId={(item) => item.id}
minColWidth={minColWidth}
maxColWidth={maxColWidth}
gap={gap}
bind:masonryWidth={width}
bind:masonryHeight={height}
>
{#snippet children(prop)}
<MasonItem /> <!-- This can be literally any item you want rendered. -->
{/snippet}
</Masonry>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment