DEV Community

Cover image for Build a masonry grid in Angular
Shuaib hasan akib
Shuaib hasan akib

Posted on

Build a masonry grid in Angular

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;
}
Enter fullscreen mode Exit fullscreen mode

But it fills columns from top to bottom:

1  4  7
2  5  8
3  6  9
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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',
    },
  ]);
}
Enter fullscreen mode Exit fullscreen mode

There are two important pieces:

  • <masonry-grid> is the container.
  • masonryGridItem identifies 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: true to the component metadata.

Don't set the item width

The grid calculates item widths itself.

Avoid doing this:

.masonry-item {
  width: 300px;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
>
Enter fullscreen mode Exit fullscreen mode

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 }">
Enter fullscreen mode Exit fullscreen mode

4. Prevent layout jumps when images load

Masonry layouts are particularly sensitive to image dimensions.

Imagine the following sequence:

  1. The grid renders.
  2. Images haven't loaded yet.
  3. Items are measured at a small height.
  4. Images download.
  5. Items suddenly become taller.
  6. 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"
/>
Enter fullscreen mode Exit fullscreen mode

These attributes establish the image's aspect ratio before the image downloads.

You can still use:

img {
  width: 100%;
  height: auto;
}
Enter fullscreen mode Exit fullscreen mode

If your API already provides image dimensions, use those values:

<img
  [src]="photo.url"
  [alt]="photo.title"
  [width]="photo.width"
  [height]="photo.height"
/>
Enter fullscreen mode Exit fullscreen mode

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"
>
Enter fullscreen mode Exit fullscreen mode

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"
>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

A stamp can be positioned using CSS:

.promo {
  position: absolute;
  top: 0;
  right: 0;
  width: 280px;
}
Enter fullscreen mode Exit fullscreen mode

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,
  ]);
}
Enter fullscreen mode Exit fullscreen mode

That's it.

You don't need:

reloadItems();
layout();
Enter fullscreen mode Exit fullscreen mode

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)
);
Enter fullscreen mode Exit fullscreen mode

When the signal changes, the masonry layout follows the DOM.

You can also listen for completed layout passes:

<masonry-grid
  (layoutComplete)="onLayout($event)"
>
Enter fullscreen mode Exit fullscreen mode
onLayout(event: MasonryLayoutEvent) {
  console.log(event.columns);
  console.log(event.itemCount);
  console.log(event.durationMs);
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
    }
  }"
>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 }"
>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
    }),
  ],
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)
);
Enter fullscreen mode Exit fullscreen mode

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:

  1. Don't set an item's width. The masonry grid owns it. Use masonryColSpan when an item needs more space.
  2. Reserve space for asynchronous content. Give images dimensions and charts an expected height or aspect ratio.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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)