Skip to content

How to Manage Breakpoints using BreakpointObserver in Angular

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Defining Breakpoints is important when you start working with Responsive Design and most of the time they're created using CSS code. For example:

.title {
  font-size: 12px;
}

@media (max-width: 600px) {
  .title {
    font-size: 14px;
  }
}

By default, the text size value will be 12px, and this value will be changed to 14px when the viewport gets changed to a smaller screen (a maximum width of 600px).

That solution works. However, what about if you need to listen for certain breakpoints to perform changes in your application? This may be needed to configure third-party components, processing events, or any other.

Luckily, Angular comes with a handy solution for these scenarios: the BreakpointObserver. Which is a utility for checking the matching state of @media queries.

In this post, we will build a sample application to add the ability to configure certain breakpoints, and being able to listen to them.

Project Setup

Prerequisites

You'll need to have installed the following tools in your local environment:

  • Node.js. Preferably the latest LTS version.
  • A package manager. You can use either NPM or Yarn. This tutorial will use NPM.

Creating the Angular Project

Let's start creating a project from scratch using the Angular CLI tool.

ng new breakpointobserver-example-angular --routing --prefix corp --style css --skip-tests

This command will initialize a base project using some configuration options:

  • --routing. It will create a routing module.
  • --prefix corp. It defines a prefix to be applied to the selectors for created components(corp in this case). The default value is app.
  • --style scss. The file extension for the styling files.
  • --skip-tests. it avoids the generations of the .spec.ts files, which are used for testing

Adding Angular Material and Angular CDK

Before creating the breakpoints, let's add the Angular Material components, which will install the Angular CDK library under the hood.

ng add @angular/material

Creating the Home Component

We can create a brand new component to handle a couple of views to be updated while the breakpoints are changing. We can do that using the ng generate command.

ng generate component home

Pay attention to the output of the previous command since it will show you the auto-generated files.

Update the Routing Configuration

Remember we used the flag --routing while creating the project? That parameter has created the main routing configuration file for the application: app-routing.module.ts. Let's update it to be able to render the home component by default.

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';

const routes: Routes = [
  {
    path: '',
    component: HomeComponent
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Update the App Component template

Remove all code except the router-outlet placeholder:

<!-- app.component.html -->
<router-outlet></router-outlet>

This will allow rendering the home component by default once the routing configuration is running.

Using the BreakpointObserver

The application has the Angular CDK installed already, which has a layout package with some utilities to build responsive UIs that react to screen-size changes.

Let's update the HomeComponent, and inject the BreakpointObserver as follows.

//home.component.ts
import { Component, OnInit } from '@angular/core';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';

@Component({
  selector: 'corp-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {

  readonly breakpoint$ = this.breakpointObserver
    .observe([Breakpoints.Large, Breakpoints.Medium, Breakpoints.Small, '(min-width: 500px)'])
    .pipe(
      tap(value => console.log(value)),
      distinctUntilChanged()
    );

  constructor(private breakpointObserver: BreakpointObserver) { }

  ngOnInit(): void {
  }
}

Once the BreakpointObserver is injected, we'll be able to evaluate media queries to determine the current screen size, and perform changes accordingly.

Then, a breakpoint$ variable references an observable object after a call to the observe method.

The observe method gets an observable of results for the given queries, and can be used along with predetermined values defined on Breakpoints as a constant.

Also, it's possible to use custom breakpoints such as (min-width: 500px). Please refer to the documentation to find more details about this.

Next, you may need to subscribe to the breakpoint$ observable to see the emitted values after matching the given queries.

Again, let's update the home.component.ts file to do that.

// home.component.ts

// .... other imports
import { distinctUntilChanged, tap } from 'rxjs/operators';

@Component({
  //....
})
export class HomeComponent implements OnInit {

  Breakpoints = Breakpoints;
  currentBreakpoint:string = '';

  // ... readonly breakpoint$ = this.breakpointObserver

  constructor(private breakpointObserver: BreakpointObserver) { }

  ngOnInit(): void {
    this.breakpoint$.subscribe(() =>
      this.breakpointChanged()
    );
  }
  private breakpointChanged() {
    if(this.breakpointObserver.isMatched(Breakpoints.Large)) {
      this.currentBreakpoint = Breakpoints.Large;
    } else if(this.breakpointObserver.isMatched(Breakpoints.Medium)) {
      this.currentBreakpoint = Breakpoints.Medium;
    } else if(this.breakpointObserver.isMatched(Breakpoints.Small)) {
      this.currentBreakpoint = Breakpoints.Small;
    } else if(this.breakpointObserver.isMatched('(min-width: 500px)')) {
      this.currentBreakpoint = '(min-width: 500px)';
    }
  }
}

In the above code, the ngOnInit method is used to perform a subscription to the breakpoint$ observable and the method breakpointChanged will be invoked every time a breakpoint match occurs.

As you may note, the breakpointChanged method verifies what Breakpoint value has a match through isMatched method. In that way, the current component can perform changes after a match happened (in this case, it just updates the value for the currentBreakpoint attribute).

Using Breakpoint values on the Template

Now, we can set a custom template in the home.component.html file and be able to render a square according to the currentBreakpoint value.

<!-- home.component.html -->

<mat-card style="text-align: center; margin: 20px">
  {{ currentBreakpoint }}
</mat-card>

<div class="container" [ngSwitch]="currentBreakpoint">
  <span class="large" *ngSwitchCase="Breakpoints.Large"> Large </span>
  <span class="medium" *ngSwitchCase="Breakpoints.Medium"> Medium </span>
  <span class="small" *ngSwitchCase="Breakpoints.Small"> Small </span>
  <span class="custom" *ngSwitchCase="'(min-width: 500px)'"> Custom </span>
</div>

The previous template will render the current media query value at the top along with a rectangle according to the size: Large, Medium, Small or Custom.

Live Demo and Source Code

Want to play around with this code? Just open the Stackblitz editor or the preview mode in fullscreen.

Find the complete angular project in this GitHub repository: breakpointobserver-example-angular. Do not forget to give it a star ⭐️ and play around with the code.

Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

End-to-end type-safety with JSON Schema cover image

End-to-end type-safety with JSON Schema

End-to-end type-safety with JSON Schema I recently wrote an introduction to JSON Schema post. If you’re unfamiliar with it, check out the post, but TLDR: It’s a schema specification that can be used to define the input and output data for your JSON API. In my post, I highlight many fantastic benefits you can reap from defining schemas for your JSON API. One of the more interesting things you can achieve with your schemas is end-to-end type safety from your backend API to your client application(s). In this post, we will explore how this can be accomplished slightly deeper. Overview The basic idea of what we want to achieve is: * a JSON API server that validates input and output data using JSON Schema * The JSON Schema definitions that our API uses transformed into TypeScript types With those pieces in place, we can achieve type safety on our API server and the consuming client application. The server side is pretty straightforward if you’re using a server like Fastify with already enabled JSON Schema support. This post will focus on the concepts more than the actual implementation details though. Here’s a simple diagram illustrating the high-level concept: We can share the schema and type declaration between the client and server. In that case, we can make a request to an endpoint where we know its type and schema, and assuming the server validates the data against the schema before sending it back to the client, our client can be confident about the type of the response data. Marrying JSON Schema and TypeScript There are a couple of different ways to accomplish this: * Generating types from schema definitions using code generation tools * Creating TypeBox definitions that can infer TypeScript types and be compiled to JSON Schema I recommend considering both and figuring out which would better fit your application and workflows. Like anything else, each has its own set of trade-offs. In my experience, I’ve found TypeBox to be the most compelling if you want to go deep with this pattern. Code generation A couple of different packages are available for generating TS types from JSON Schema definitions. * https://github.com/bcherny/json-schema-to-typescript * https://github.com/vega/ts-json-schema-generator They are CLI tools that you can provide a glob path to where your schema files are located and will generate TS declaration files to a specified output path. You can set up an npm hook or a similar type of script that will generate types for your development environment. TypeBox TypeBox is a JSON Schema type builder. With this approach, instead of json files, we define schemas in code using the TypeBox API. The TypeBox definitions infer to TypeScript types directly, which eliminates the code generation step described above. Here’s a simple example from the documentation of a JSON Schema definition declared with TypeBox: ` This can then be inferred as a TypeScript type: ` Aside from schemas and types, TypeBox can do a lot more to help us on our type-safety journey. We will explore it a bit more in upcoming sections. Sharing schemas between client and server applications Sharing our JSON Schema between our server and client app is the main requirement for end-to-end type-safety. There are a couple of different ways to accomplish this, but the simplest would be to set up our codebase as a monorepo that contains both the server and client app. Some popular options for TypeScript monorepos are: PNPM, Turborepo, and NX. If a monorepo is not an option, you can publish your schema and types as a package that can be installed in both projects. However, this setup would require a lot more maintenance work. Ultimately, as long as you can import your schemas and types from the client and server app, you are in good shape. Server-to-client validation and type-safety For the sake of simplicity, let's focus on data flowing from the server to the client for now. Generally speaking, the concepts also apply in reverse, as long as your JSON API server validates your inputs and outputs. We’ll look at the most basic version of having strongly typed data on the client from a request to our server. Type-safe client requests In our server application, if we validate the /users endpoint with a shared schema - on the client side, when we make the request to the endpoint, we know that the response data is validated using the user schema. As long as we are confident of this fact, we can use the generated type from that schema as the return type on our client fetch call. Here’s some pseudocode: ` Our server endpoint would look something like this: ` You could get creative and build out a map that defines all of your endpoints, their metadata, and schemas, and use the map to define your server endpoints and create an API client. Transforming data over the wire Everything looks stellar, but we can still take our efforts a bit further. To this point, we are still limited to serialized JSON data. If we have a created_at field (number or ISO string) tied to our user, and we want it to be a Date object when we get a hold of it on the client side - additional work and consideration are required. There are some different strategies out there for deserializing JSON data. The great thing about having shared schemas between our client and server is that we can encode our type information in the schema without sending additional metadata from our server to the client. Using format to declare type data In my initial JSON Schema blog post, I touched on the format field of the specification. In our schema, if the actual type of our date is a string in ISO8601 format, we can declare our format to be "date-time". We can use this information on the client to transform the field into a proper Date object. ` Transforming serialized JSON Data This can be a little bit tricky; again, there are many ways to accomplish it. To demonstrate the concept, we’ll use TypeBox to define our schemas as discussed above. TypeBox provides a Transform type that you can use to declare, encode, and decode methods for your schema definition. ` It even provides helpers to statically generate the decoded and encoded types for your schema ` If you declare your decode and encode functions for your schemas, you can then use the TypeBox API to handle decoding the serialized values returned from your JSON API. Here’s what the concept looks like in practice fetching a user from our API: ` Nice. You could use a validation library like Zod to achieve a similar result but here we aren’t actually doing any validation on our client side. That happened on the server. We just know the types based on the schema since both ends share them. On the client, we are just transforming our serialized JSON into what we want it to be in our client application. Summary There are a lot of pieces in play to accomplish end-to-end type safety. With the help of JSON Schema and TypeBox though, it feels like light work for a semi-roll-your-own type of solution. Another great thing about it is that it’s flexible and based on pretty core concepts like a JSON API paired with a TypeScript-based client application. The number of benefits that you can reap from defining JSON Schemas for your APIs is really great. If you’re like me and wanna keep it simple by avoiding GraphQL or other similar tools, this is a great approach....

Exploring Angular Forms: A New Alternative with Signals cover image

Exploring Angular Forms: A New Alternative with Signals

Exploring Angular Forms: A New Alternative with Signals In the world of Angular, forms are essential for user interaction, whether you're crafting a simple login page or a more complex user profile interface. Angular traditionally offers two primary approaches: template-driven forms and reactive forms. In my previous series on Angular Reactive Forms, I explored how to harness reactive forms' power to manage complex logic, create dynamic forms, and build custom form controls. A new tool for managing reactivity - signals - has been introduced in version 16 of Angular and has been the focus of Angular maintainers ever since, becoming stable with version 17. Signals allow you to handle state changes declaratively, offering an exciting alternative that combines the simplicity of template-driven forms with the robust reactivity of reactive forms. This article will examine how signals can add reactivity to both simple and complex forms in Angular. Recap: Angular Forms Approaches Before diving into the topic of enhancing template-driven forms with signals, let’s quickly recap Angular's traditional forms approaches: 1. Template-Driven Forms: Defined directly in the HTML template using directives like ngModel, these forms are easy to set up and are ideal for simple forms. However, they may not provide the fine-grained control required for more complex scenarios. Here's a minimal example of a template-driven form: ` ` 2. Reactive Forms: Managed programmatically in the component class using Angular's FormGroup, FormControl, and FormArray classes; reactive forms offer granular control over form state and validation. This approach is well-suited for complex forms, as my previous articles on Angular Reactive Forms discussed. And here's a minimal example of a reactive form: ` ` Introducing Signals as a New Way to Handle Form Reactivity With the release of Angular 16, signals have emerged as a new way to manage reactivity. Signals provide a declarative approach to state management, making your code more predictable and easier to understand. When applied to forms, signals can enhance the simplicity of template-driven forms while offering the reactivity and control typically associated with reactive forms. Let’s explore how signals can be used in both simple and complex form scenarios. Example 1: A Simple Template-Driven Form with Signals Consider a basic login form. Typically, this would be implemented using template-driven forms like this: ` ` This approach works well for simple forms, but by introducing signals, we can keep the simplicity while adding reactive capabilities: ` ` In this example, the form fields are defined as signals, allowing for reactive updates whenever the form state changes. The formValue signal provides a computed value that reflects the current state of the form. This approach offers a more declarative way to manage form state and reactivity, combining the simplicity of template-driven forms with the power of signals. You may be tempted to define the form directly as an object inside a signal. While such an approach may seem more concise, typing into the individual fields does not dispatch reactivity updates, which is usually a deal breaker. Here’s an example StackBlitz with a component suffering from such an issue: Therefore, if you'd like to react to changes in the form fields, it's better to define each field as a separate signal. By defining each form field as a separate signal, you ensure that changes to individual fields trigger reactivity updates correctly. Example 2: A Complex Form with Signals You may see little benefit in using signals for simple forms like the login form above, but they truly shine when handling more complex forms. Let's explore a more intricate scenario - a user profile form that includes fields like firstName, lastName, email, phoneNumbers, and address. The phoneNumbers field is dynamic, allowing users to add or remove phone numbers as needed. Here's how this form might be defined using signals: ` > Notice that the phoneNumbers field is defined as a signal of an array of signals. This structure allows us to track changes to individual phone numbers and update the form state reactively. The addPhoneNumber and removePhoneNumber methods update the phoneNumbers signal array, triggering reactivity updates in the form. ` > In the template, we use the phoneNumbers signal array to dynamically render the phone number input fields. The addPhoneNumber and removePhoneNumber methods allow users to reactively add or remove phone numbers, updating the form state. Notice the usage of the track function, which is necessary to ensure that the ngFor directive tracks changes to the phoneNumbers array correctly. Here's a StackBlitz demo of the complex form example for you to play around with: Validating Forms with Signals Validation is critical to any form, ensuring that user input meets the required criteria before submission. With signals, validation can be handled in a reactive and declarative manner. In the complex form example above, we've implemented a computed signal called formValid, which checks whether all fields meet specific validation criteria. The validation logic can easily be customized to accommodate different rules, such as checking for valid email formats or ensuring that all required fields are filled out. Using signals for validation allows you to create more maintainable and testable code, as the validation rules are clearly defined and react automatically to changes in form fields. It can even be abstracted into a separate utility to make it reusable across different forms. In the complex form example, the formValid signal ensures that all required fields are filled and validates the email and phone numbers format. This approach to validation is a bit simple and needs to be better connected to the actual form fields. While it will work for many use cases, in some cases, you might want to wait until explicit "signal forms" support is added to Angular. Tim Deschryver started implementing some abstractions around signal forms, including validation and wrote an article about it. Let's see if something like this will be added to Angular in the future. Why Use Signals in Angular Forms? The adoption of signals in Angular provides a powerful new way to manage form state and reactivity. Signals offer a flexible, declarative approach that can simplify complex form handling by combining the strengths of template-driven forms and reactive forms. Here are some key benefits of using signals in Angular forms: 1. Declarative State Management: Signals allow you to define form fields and computed values declaratively, making your code more predictable and easier to understand. 2. Reactivity: Signals provide reactive updates to form fields, ensuring that changes to the form state trigger reactivity updates automatically. 3. Granular Control: Signals allow you to define form fields at a granular level, enabling fine-grained control over form state and validation. 4. Dynamic Forms: Signals can be used to create dynamic forms with fields that can be added or removed dynamically, providing a flexible way to handle complex form scenarios. 5. Simplicity: Signals can offer a simpler, more concise way to manage form states than traditional reactive forms, making building and maintaining complex forms easier. Conclusion In my previous articles, we explored the powerful features of Angular reactive forms, from dynamic form construction to custom form controls. With the introduction of signals, Angular developers have a new tool that merges the simplicity of template-driven forms with the reactivity of reactive forms. While many use cases warrant Reactive Forms, signals provide a fresh, powerful alternative for managing form state in Angular applications requiring a more straightforward, declarative approach. As Angular continues to evolve, experimenting with these new features will help you build more maintainable, performant applications. Happy coding!...

How to Build a Slideshow App Using Swiper and Angular cover image

How to Build a Slideshow App Using Swiper and Angular

Google Slides and Microsoft PowerPoint are the most popular options to build presentations nowadays. However, have you ever considered having a custom tool where you can use your web development knowledge to create beautiful slides? In this post, we will build a slideshow app with the ability to render one slide at a time using Angular, Swiper, and a little bit of CSS. Project Setup Prerequisites You'll need to have installed the following tools in your local environment: - Node.js. Preferably the latest LTS version. - A package manager. You can use either NPM or Yarn. This tutorial will use NPM. Creating the Angular Project Let's start creating a project from scratch using the Angular CLI tool. ` This command will initialize a base project using some configuration options: - --routing. It will create a routing module. - --prefix corp. It defines a prefix to be applied to the selectors for created components(corp in this case). The default value is app. - --style scss. The file extension for the styling files. - --skip-tests. It avoids the generations of the .spec.ts files, which are used for testing. Creating the Slides Component We can create a brand new component to handle the content of the application's slides. We can do that using the command mg generate as follows: ` The output of the previous command will show the auto-generated files. Update the Routing Configuration Remember we used the flag --routing while creating the project? That parameter has created the main routing configuration file for the application: app-routing.module.ts. Let's update it to be able to render the slides component by default. ` Update the App Component template Remove all code except the router-outlet placeholder: ` This will allow you to render the slides component by default once the routing configuration is running. Using Swiper What is Swiper? Swiper is a popular JavaScript library that lets you create transitions that can work on websites, mobile web apps, and mobile native/hybrid apps. It's available for all modern frameworks and it's powered with top-notch features you can find on the official website. Installing Swiper The plugin is available via NPM and you can use the following command to install it in the project. ` Updating the Application Module Next, we'll need to update the application module before starting to use Swiper. ` Using swiper element in the Template It's time to work in the slide component, and make use of the plugin along with the available options. ` For a better understanding, let's describe what's happening in the above template: * The element will create a Swiper instance to be rendered in the component. * slidesPerView will set the number of slides visible at the same time. * navigation will render the navigation buttons to slide to the left and right. * pagination will set the pagination configuration through an object. * keyboard will enable navigation through the keyboard. * virtual will enable the virtual slides feature. This is important in case you have several slides, and you want to limit the amount of them to be rendered in the DOM. * class will set a class to customize the styling. * swiperSlide is an Angular directive that helps to render a slide instance. One important note here is the custom container created under the swiperSlide directive allows us to customize the way we can render every slide. In this case, it's used to set a layout for every slide, and make sure to render it centered, and using the whole height of the viewport. Set the Swiper Configuration As you may note, the template will require additional configurations, and we'll need to create a couple of slides for the component. ` In the above code snippet, the component imports the SwiperCore class along with the required modules. Next, the component needs to install those using a SwiperCore.use([]) call. Later, a BehaviorSubject is created to emit all slides content once the component gets initialized. Using Swiper Styles Since the current application is configured to use SCSS styles, we'll need to import the following styles into the slides.component.scss file: ` Of course, these imports will work with the latest version of Swiper(version 8 at the time of writing this article). However, some users have found some issues while importing Swiper styles (mainly after doing an upgrade from Swiper v6 and v7). For example: ` or an error like this: ` If you got any of those issues, you can give it a try with the following imports instead: ` For the purpose of this demo, we'll attach additional styling to make it work: ` The .my-swiper selector will set the appropriate height for every slide. On other hand, the .swiper-slide-container selector will provide a layout as a slide container. And this is how it will look on your web browser. Live Demo and Source Code Want to play around with the final application? Just open the following link in your browser: https://luixaviles.github.io/slideshow-angular-swiper. Find the complete angular project in this GitHub repository: slideshow-angular-swiper. Do not forget to give it a star ⭐️ and play around with the code. Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work....

Making AI Deliver: From Pilots to Measurable Business Impact cover image

Making AI Deliver: From Pilots to Measurable Business Impact

A lot of organizations have experimented with AI, but far fewer are seeing real business results. At the Leadership Exchange, this panel focused on what it actually takes to move beyond experimentation and turn AI into measurable ROI. Over the past few years, many organizations have experimented with AI, but the challenge today is translating experimentation into measurable business value. Moderated by Tracy Lee, CEO at This Dot Labs, panelists featured Dorren Schmitt, Vice President IT Strategy & Innovation at Allen Media Group, Greg Geodakyan, CTO at Client Command, and Elliott Fouts, CAIO & CTO at This Dot Labs. Panelists discussed how companies are moving from early AI experiments to initiatives that deliver real results. They began by examining how experimentation has evolved over the past year. While many organizations did not fully utilize AI experimentation budgets in 2025, 2026 is showing a shift toward more intentional investment. Structured budgets and clearly defined frameworks are enabling companies to explore AI strategically and identify initiatives with high potential impact. The conversation then turned to alignment and ROI. Panelists highlighted the importance of connecting AI projects to corporate strategy and leadership priorities. Ensuring that AI initiatives translate into operational efficiency, productivity gains, and measurable business impact is essential. Companies that successfully align AI efforts with organizational goals are better equipped to demonstrate tangible outcomes from their investments. Moving from pilots and proofs of concept to production was another major focus. Governance, prioritization, and workflow integration were cited as essential for scaling AI initiatives. One panelist shared that out of nine proofs of concept, eight successfully launched, resulting in improvements in quality and operational efficiency. Panelists also explored the future of AI within organizations, including the potential for agentic workflows and reduced human-in-the-loop processes. New capabilities are emerging that extend beyond coding tasks, reshaping how teams collaborate and how work is structured across departments. Key Takeaways - Structured experimentation and defined budgets allow organizations to explore AI strategically and safely. - Alignment with business priorities is essential for translating AI capabilities into measurable outcomes. - Governance and workflow integration are critical to moving AI initiatives from pilot stages to production deployment. Successfully leveraging AI requires a balance between experimentation, strategic alignment, and operational discipline. Organizations that approach AI as a structured, measurable initiative can capture meaningful results and unlock new opportunities for innovation. Curious how your organization can move from AI experimentation to real impact? Let’s talk. Reach out to continue the conversation or join us at an upcoming Leadership Exchange. Tracy can be reached at tlee@thisdot.co....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co