Masonry layouts are useful anywhere you have items with different heights but still want a compact, flowing grid.
Common examples include:
- Photo and image galleries — images naturally have different aspect ratios.
- Dashboards — KPI cards, charts, tables, and widgets rarely have the same height.
- Product and catalog pages — product descriptions and images can vary in size.
- Blog and content feeds — cards often contain different amounts of text.
- Portfolio and project grids — projects can have very different visual dimensions.
- Search and discovery pages — results can vary significantly in height.
- Admin and analytics interfaces — widgets often have dynamic content and dimensions.
A normal CSS grid lays items out in rows. If one item is taller than the others, the entire row has to accommodate it, leaving empty space underneath shorter items.
Masonry solves that by placing each item into the column with the least available height.
In this guide, we'll build a masonry grid in Angular and gradually add responsive columns, dynamic content, spanning, SSR support, and native CSS masonry.
By the end, you'll have a grid that:
- responds to any screen size
- supports items spanning multiple columns
- automatically reacts to added, removed, or resized items
- avoids layout jumps when images load
- works with Angular SSR
- can use native CSS masonry where available
- has zero runtime dependencies
If you want to see the finished result first, check out the live demo.
Why use a library?
Before writing JavaScript, it is worth asking whether CSS can already solve the problem.
There are two common approaches.
CSS columns
column-count gives you a masonry-like appearance with very little CSS:
.gallery {
column-count: 3;
}
But it fills columns from top to bottom:
1 4 7
2 5 8
3 6 9
That can be problematic when the order of your content matters. A list sorted by date or search relevance no longer reads naturally across the page.
CSS Grid
CSS Grid preserves the expected DOM order:
.gallery {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
But it still works in rows. A tall item determines the height of its entire row, leaving empty space underneath shorter items.
To get both normal item order and compact masonry packing, something needs to measure the items and decide where each one belongs.
That's the job of masonry-angular.
Native CSS masonry is arriving through
display: grid-lanes. We'll look at that near the end of the article.
1. Install
Install the library with npm:
npm install masonry-angular
There are no additional runtime dependencies.
The library supports Angular 17.1 and later, including Angular 22.
2. Build your first grid
Import the masonry directives into your component:
import { Component, signal } from '@angular/core';
import { NG_MASONRY_GRID } from 'masonry-angular';
@Component({
selector: 'app-gallery',
imports: [NG_MASONRY_GRID],
template: `
<masonry-grid columns="3" gutter="16">
@for (photo of photos(); track photo.id) {
<article masonryGridItem>
<img
[src]="photo.url"
[alt]="photo.title"
/>
<h3>{{ photo.title }}</h3>
</article>
}
</masonry-grid>
`,
})
export class Gallery {
readonly photos = signal([
{
id: 1,
url: 'https://picsum.photos/id/10/400/300',
title: 'Forest',
},
{
id: 2,
url: 'https://picsum.photos/id/20/400/520',
title: 'Desk',
},
{
id: 3,
url: 'https://picsum.photos/id/30/400/260',
title: 'Coffee',
},
{
id: 4,
url: 'https://picsum.photos/id/40/400/440',
title: 'Street',
},
]);
}
There are two important pieces:
-
<masonry-grid>is the container. -
masonryGridItemidentifies each child as an item.
columns="3" creates three columns, while gutter="16" adds a 16px gap.
NG_MASONRY_GRID exports the directives needed by the library, so you can import them together.
If you're using Angular 17 or 18, add
standalone: trueto the component metadata.
Don't set the item width
The grid calculates item widths itself.
Avoid doing this:
.masonry-item {
width: 300px;
}
A manually assigned width fights the layout engine and can cause overlapping or incorrect positioning.
You can style everything else normally:
article[masonryGridItem] img {
width: 100%;
display: block;
border-radius: 8px;
}
Let the grid own the width.
3. Make it responsive
A fixed column count isn't ideal across different screen sizes.
Instead of manually writing breakpoints, you can use columnWidth:
<masonry-grid columnWidth="260" gutter="16">
This means:
Make columns approximately 260px wide and fit as many as possible.
So the same grid might automatically become:
Desktop: 5 columns
Tablet: 3 columns
Mobile: 1 column
You don't need to maintain a breakpoint table.
If you need exact column counts, you can define them explicitly:
<masonry-grid
[columns]="{
'0': 1,
'768': 2,
'1200': 4
}"
gutter="16"
>
This means:
- 1 column from
0px - 2 columns from
768px - 4 columns from
1200px
You can also use named breakpoints:
<masonry-grid [columns]="{ xs: 1, md: 2, xl: 4 }">
4. Prevent layout jumps when images load
Masonry layouts are particularly sensitive to image dimensions.
Imagine the following sequence:
- The grid renders.
- Images haven't loaded yet.
- Items are measured at a small height.
- Images download.
- Items suddenly become taller.
- The grid has to rearrange everything.
The result is a visible layout shift.
The simplest solution is to tell the browser the image dimensions up front:
<img
[src]="photo.url"
[alt]="photo.title"
width="400"
height="300"
/>
These attributes establish the image's aspect ratio before the image downloads.
You can still use:
img {
width: 100%;
height: auto;
}
If your API already provides image dimensions, use those values:
<img
[src]="photo.url"
[alt]="photo.title"
[width]="photo.width"
[height]="photo.height"
/>
For content where the dimensions are genuinely unknown, use an appropriate CSS aspect-ratio.
The goal is simple: reserve the space before the content arrives.
5. Span multiple columns
Sometimes a particular item needs more space.
For example, a featured photo can span two columns:
<article
masonryGridItem
[masonryColSpan]="photo.featured ? 2 : 1"
>
Use masonryColSpan rather than trying to increase the CSS width.
The grid needs to know that the item occupies two columns so it can correctly pack the other items around it.
If the viewport only has one column, a span of 2 is automatically clamped so the item doesn't overflow.
You can also temporarily remove an item from the layout:
<article
masonryGridItem
[masonryIgnore]="photo.hidden"
>
The element remains in the DOM, but the masonry engine ignores it.
6. Let items flow around a fixed element
Another useful pattern is a pinned element that other items should flow around.
For example, you might have a promotional card:
<masonry-grid columnWidth="260" gutter="16">
<aside masonryGridStamp class="promo">
<h4>Get the weekly digest</h4>
</aside>
@for (photo of photos(); track photo.id) {
<article masonryGridItem>
...
</article>
}
</masonry-grid>
A stamp can be positioned using CSS:
.promo {
position: absolute;
top: 0;
right: 0;
width: 280px;
}
The grid takes the stamp's position into account and packs normal items around it.
This is useful for ads, promotional cards, subscription boxes, or other fixed content inside a masonry layout.
7. Dynamic updates just work
One of the goals of the library is to make dynamic Angular applications feel natural.
For example:
addPhoto() {
this.photos.update((photos) => [
...photos,
newPhoto,
]);
}
That's it.
You don't need:
reloadItems();
layout();
You don't need a setTimeout() to wait for the DOM.
The grid observes its children and automatically reflows when items are:
- added
- removed
- reordered
- resized
This is especially useful for Angular applications where the content is controlled by signals, API responses, filtering, or conditional templates.
For example:
readonly visiblePhotos = computed(() =>
this.photos().filter(photo => !photo.hidden)
);
When the signal changes, the masonry layout follows the DOM.
You can also listen for completed layout passes:
<masonry-grid
(layoutComplete)="onLayout($event)"
>
onLayout(event: MasonryLayoutEvent) {
console.log(event.columns);
console.log(event.itemCount);
console.log(event.durationMs);
}
And the grid exposes state through a signal:
<masonry-grid
#grid="masonryGrid"
columnWidth="260"
>
...
</masonry-grid>
<p>
{{ grid.state().columns }} columns,
{{ grid.state().itemCount }} items
</p>
8. SSR without breaking the first render
Masonry libraries often depend on browser APIs such as window and ResizeObserver.
Those APIs don't exist on the server.
masonry-angular handles this by rendering a CSS-columns fallback during SSR.
The server can therefore produce usable HTML without needing to measure the items.
When the browser loads the application, the library switches to the actual masonry layout.
You can control the server fallback:
<masonry-grid
columnWidth="260"
[options]="{
ssr: {
fallback: 'columns',
columns: 2
}
}"
>
The common layout settings such as columns, columnWidth, gutter, gutterX, and gutterY can be provided directly. More advanced configuration lives under options.
This also makes the component suitable for prerendered Angular applications, where the generated HTML can be served without running a JavaScript masonry engine on the server.
9. Use native CSS masonry where available
CSS masonry is moving into the platform through:
display: grid-lanes;
When native masonry is available, there's little reason to run a JavaScript layout engine.
You can opt into native support:
<masonry-grid
columnWidth="260"
gutter="16"
[options]="{ native: true }"
>
The library uses native CSS masonry where supported and falls back to its JavaScript layout engine elsewhere.
That gives you a useful progressive-enhancement model:
Native masonry available
↓
Browser handles it
Native masonry unavailable
↓
JS layout engine
There is one important trade-off: when native layout takes over, features that depend on the JavaScript packing algorithm, such as stamps and masonryIgnore, aren't available.
If you need those features, keep native mode disabled.
10. Set defaults globally
If every masonry grid in your application uses the same defaults, configure them once:
import { provideNgMasonryGrid } from 'masonry-angular';
bootstrapApplication(App, {
providers: [
provideNgMasonryGrid({
gutter: 16,
columnWidth: 260,
native: true,
}),
],
});
Individual grids can still override these values when necessary.
This is particularly useful for design-system or dashboard applications where masonry configuration is consistent throughout the application.
Masonry for dashboards
Everything above also applies to dashboards.
Instead of photos, your items might be:
- KPI cards
- charts
- tables
- activity feeds
- statistics
- notifications
For example:
<masonry-grid columnWidth="320" gutter="16">
@for (card of visibleCards(); track card.id) {
<article
masonryGridItem
[masonryColSpan]="card.wide ? 2 : 1"
>
<h3>{{ card.title }}</h3>
<my-chart [data]="card.data" />
</article>
}
</masonry-grid>
Don't force every card to the same height
Avoid arbitrary fixed heights when the content is naturally variable.
Let the card determine its height, and let masonry pack the next item underneath it.
For charts that render asynchronously, however, you should reserve their expected space:
.chart {
height: 220px;
/* or */
aspect-ratio: 16 / 9;
}
This prevents the grid from laying out against a zero-height chart and then moving everything when the chart finally renders.
The same principle applies to lazy-loaded content such as Angular's @defer.
Filtering and reordering
Dashboard layouts are often dynamic:
readonly visibleCards = computed(() =>
this.cards().filter(card => card.visible)
);
When cards are filtered, reordered, added, or removed, the grid automatically recalculates the layout.
You don't need to manually synchronize the masonry engine with your application state.
Three things to remember
If you only remember three things from this article, make them these:
-
Don't set an item's width. The masonry grid owns it. Use
masonryColSpanwhen an item needs more space. - Reserve space for asynchronous content. Give images dimensions and charts an expected height or aspect ratio.
- Don't manually trigger layouts. Change your Angular state and let the grid react to the DOM.
Try it
Install it with:
npm install masonry-angular
- Live demo: https://masonry-angular.vercel.app
- StackBlitz: https://stackblitz.com/github/MeAkib/masonry-angular/tree/main/examples/stackblitz
- npm: https://www.npmjs.com/package/masonry-angular
- GitHub: https://github.com/MeAkib/masonry-angular
- Documentation: https://github.com/MeAkib/masonry-angular/blob/main/projects/masonry-angular/DOCS.md
masonry-angular is still early in its development, so the API may evolve.
If you try it and something feels harder than it should, open an issue or start a discussion. Real-world feedback is especially useful at this stage.
The project was created to provide a modern Angular-native masonry solution without requiring a separate masonry engine or runtime dependencies.
Top comments (0)