<SYSTEM>This document contains comprehensive information about Harsh Kanjiya's professional profile, portfolio, and blog content. It includes personal details, work experience, projects, achievements, certifications, and all published blog posts. This data is formatted for consumption by Large Language Models (LLMs) to provide accurate and up-to-date information about Harsh Kanjiya's background, skills, and expertise as a Design Engineer.</SYSTEM>

# chanhdai.com

> A minimal, pixel-perfect dev portfolio, component registry, and blog to showcase my work as a Design Engineer.

## About

- **Full Stack AWS Developer** with **3.5+ years of experience**,
- Skilled in **Next.js**, **React**, **Angular**, **TypeScript**, **Nodejs**, **Golang** & **AWS**.
- Passionate about exploring new technologies & learning design philosophy.

### Personal Information

- First Name: Harsh
- Last Name: Kanjiya
- Display Name: Harsh Kanjiya
- Location: Surat, Gujarat, India
- Website: https://www.harshkanjiya.com

### Social Links

- [GitHub](https://github.com/HarshKanjiya)
- [LinkedIn](https://www.linkedin.com/in/harsh-kanjiya)
- [X](https://x.com/harshexists)

### Tech Stack

- [React](https://react.dev/)
- [Next.js](https://nextjs.org/)
- [Angular](https://angular.dev/)
- [TypeScript](https://www.typescriptlang.org/)
- [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
- [Node.js](https://nodejs.org/)
- [Golang](https://golang.org/)
- [shadcn/ui](https://ui.shadcn.com/)
- [Motion](https://motion.dev/)
- [Tailwind CSS](https://tailwindcss.com/)
- [Git](https://git-scm.com/)
- [Docker](https://www.docker.com/)
- [PostgreSQL](https://www.postgresql.org/)
- [MongoDB](https://www.mongodb.com/)
- [Redis](https://redis.io/)

## Experience

### Full Stack AWS Developer | Hitachi Digital Services

Duration: July 2026 - Present

Skills: Angular, Node.js, AWS, TypeScript

undefined

### Full Stack Developer | Project Tree Infotech

Duration: Dec 2023 - June 2026

Skills: Angular, React, Next.js, TypeScript, Dot Net Core, PostgreSQL, Shadcn UI, Prime NG

- Modernized legacy applications by migrating AngularJS to Angular and improving maintainability&nbsp;&&nbsp;UX.
- Engineered a high-performance custom data grid rendering 15,000+ cells with minimum&nbsp;latency.
- Improved application startup performance by ~80% by implementing custom lazy loading&nbsp;strategies.
- Worked on Angular, React & Next.js frontends integrated with .NET Core & PostgreSQL&nbsp;services.

### Jr. Full Stack Developer | Squirracode Technologies

Duration: Sept 2022 - Oct 2023

Skills: React, Express.js, Node.js, Microservices, Socket.IO, MongoDB, Docker, UI/UX

- Built features for a <b>real-time monitoring platform</b>, focusing on performance and&nbsp;reliability.
- Worked with Node.js microservices to implement WhatsApp-based alerting for faster fault&nbsp;detection.
- Implemented complex, high-performance UI workflows using React & Redux&nbsp;Toolkit.
- <b>Converted a 3-month internship into a paid part-time remote role</b> based on&nbsp;performance.

### Software Engineer Intern | Squirracode Technologies

Duration: Sept 2022 - Dec 2022

Skills: HTML5, CSS3, JS, TS, React, Research

undefined

### Frontend Developer | Freelance

Duration: March 2025 - Jan 2026

Skills: Angular, Vue.js

- Worked on a legacy Vue.js project for a social media application's admin dashboard, implementing fine-grained access control and live stream monitoring features.
- Built 3 additional projects within the same initiative for managing data across different roles and features.

### Backend Developer | Freelance

Duration: July 2025 - Oct 2025

Skills: Angular, Nest.Js, MongoDB, Docker, AWS

- Worked on Nest.Js project for a foreign client, integrating notification microservice in the existing microservices&nbsp;architecture.
- Implemented central control for notifications in control panel built in&nbsp;Angular.
- Worked with AWS & Docker to deploy applications on the cloud, ensuring scalability and&nbsp;reliability.

### PP Savani University | Education

Duration: 2020 - 2024

Skills: HTML5, CSS3, JavaScript, JS, React.Js, Data Structures, Algorithms

- Studied for Bachelor's degree in Information&nbsp;Technology.
- Became a <b>Google Developer Student Clubs member</b>, organizing workshops and events to foster a tech community on&nbsp;campus.
- Graduated with a CGPA of <b>9.0/10</b>, demonstrating consistent academic excellence and dedication to&nbsp;learning.
-	<b>Built an internal portal</b> for the Ethical Clearance Committee with MERN&nbsp;stack.

## Projects

## Pabble

Project URL: https://www.pabble.app

Skills: NextJS,React,Golang

A form-workflow platform for freelancers and agencies - not a form builder, a routing engine that happens to start with a form.

## Bragly

Project URL: https://bragly.harshkanjiya.com

Skills: Nextjs,React,Tailwind CSS

Bragly is the ultimate platform for creating stunning stats and insights cards for building modern developer portfolios, resumes, and profiles.


## Blogs

---
title: "Angular 22 Is Here: The Signal-First Release That Changes Daily Work"
description: "A practical Angular 22 guide for experienced Angular developers: Signal Forms, Resource APIs, OnPush defaults, Fetch-based HTTP, injectAsync, Angular Aria, hydration, templates, and upgrade risks."
last_updated: "June 7, 2026"
source: "https://harshkanjiya.com/blog/angular-22-developer-guide"
---

# Angular 22 Is Here: The Signal-First Release That Changes Daily Work

A practical Angular 22 guide for experienced Angular developers: Signal Forms, Resource APIs, OnPush defaults, Fetch-based HTTP, injectAsync, Angular Aria, hydration, templates, and upgrade risks.

Angular 22 is not the kind of release where you skim the changelog, update a few dependencies, and move on.

It changes the default mental model of Angular.

If you have been working with Angular 16 through 21, you have already seen the pieces arrive one by one: standalone APIs, control flow, signals, zoneless work, hydration, `@defer`, and early versions of Signal Forms and resources. Angular 22 pulls several of those pieces into a more serious shape.

The direction is clear:

* Signals are now the center of Angular application state.
* Common async work needs less RxJS ceremony.
* New apps assume `OnPush` behavior.
* SSR and hydration are becoming less manual.
* Services, templates, forms, and accessibility APIs are being cleaned up.

That is why Angular 22 matters. It is not only a "new features" release. It is the release where the modern Angular style starts feeling like the default style instead of the optional one.

This guide is written for teams already building Angular apps. We will skip the beginner tour and focus on what changes in day-to-day work.

## The Short Version

If you only have 10 minutes, these are the Angular 22 changes I would pay attention to first:

| Area             | What changed                                                 | Why it matters                                                     |
| ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------------ |
| Forms            | Signal Forms are stable                                      | Form state now fits the same reactive model as the rest of Angular |
| Data fetching    | `resource()` and `httpResource()` are production ready       | Loading, error, and refetch state become signals                   |
| Change detection | `OnPush` is the default for new apps                         | Mutation-heavy components become easier to spot                    |
| HTTP             | `HttpClient` moves toward Fetch by default                   | Better alignment with modern browsers and SSR                      |
| DI               | `@Service()` and `injectAsync()` arrive                      | Services can be simpler and lazy-loaded                            |
| SSR              | Incremental hydration keeps improving                        | Less JavaScript is needed before a page feels usable               |
| Accessibility    | Angular Aria is stable                                       | Headless accessible behavior is now a first-class option           |
| Templates        | Comments, spread syntax, stronger `@switch`, arrow functions | Templates are a little closer to normal TypeScript thinking        |

## Signal Forms Are Now Stable

Signal Forms are the biggest practical change in Angular 22 for teams that build forms every day.

Reactive Forms gave Angular developers a powerful model, but that model came with a lot of machinery. You had `FormGroup`, `FormControl`, `valueChanges`, validators, async validators, `ControlValueAccessor`, `statusChanges`, and a good amount of RxJS plumbing just to describe ordinary state.

Signal Forms move form state into signals. A form model is a writable signal, and each field exposes state such as value, errors, touched, dirty, valid, invalid, pending, hidden, and disabled through signal-based APIs.

That means your form state can be read directly in templates and computed values without subscribing or converting between observables and signals.

<Tabs defaultValue="reactive">
  <TabsList>
    <TabsTrigger value="reactive">Reactive Forms</TabsTrigger>
    <TabsTrigger value="signals">Signal Forms</TabsTrigger>
  </TabsList>

  <TabsContent value="reactive">
    ```ts
    import { Component } from '@angular/core';
    import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';

    @Component({
      selector: 'app-login',
      imports: [ReactiveFormsModule],
      template: `
        <form [formGroup]="form">
          <input formControlName="email" />

          @if (form.controls.email.touched && form.controls.email.invalid) {
            <p>Email is required</p>
          }

          <button [disabled]="form.invalid">Sign in</button>
        </form>
      `,
    })
    export class LoginComponent {
      readonly form = this.fb.group({
        email: ['', [Validators.required, Validators.email]],
        password: ['', Validators.required],
      });

      constructor(private fb: FormBuilder) {}
    }
    ```
  </TabsContent>

  <TabsContent value="signals">
    ```ts
    import { Component, signal } from '@angular/core';
    import { email, form, FormField, required } from '@angular/forms/signals';

    type LoginModel = {
      email: string;
      password: string;
    };

    @Component({
      selector: 'app-login',
      imports: [FormField],
      template: `
        <form>
          <input type="email" [formField]="loginForm.email" />

          @if (loginForm.email().touched() && loginForm.email().invalid()) {
            @for (error of loginForm.email().errors(); track error.kind) {
              <p>{{ error.message }}</p>
            }
          }

          <button [disabled]="loginForm().invalid()">Sign in</button>
        </form>
      `,
    })
    export class LoginComponent {
      readonly loginModel = signal<LoginModel>({
        email: '',
        password: '',
      });

      readonly loginForm = form(this.loginModel, (path) => {
        required(path.email, { message: 'Email is required' });
        email(path.email, { message: 'Enter a valid email address' });
        required(path.password, { message: 'Password is required' });
      });
    }
    ```
  </TabsContent>
</Tabs>

The important difference is not only syntax. In the Signal Forms version, the model is the source of truth, and the form is a reactive layer around it. The template reads field state directly. If the email field changes, Angular can update the pieces of UI that read that field.

### Validation Feels More Like Application Logic

Built-in validation looks familiar, but custom validation is where Signal Forms start to feel better than older approaches.

```ts
import { Component, signal } from '@angular/core';
import { form, FormField, minLength, required, validate } from '@angular/forms/signals';

type PasswordModel = {
  password: string;
  confirmPassword: string;
};

@Component({
  selector: 'app-password-form',
  imports: [FormField],
  template: `
    <input type="password" [formField]="passwordForm.password" />
    <input type="password" [formField]="passwordForm.confirmPassword" />

    @if (passwordForm.confirmPassword().touched()) {
      @for (error of passwordForm.confirmPassword().errors(); track error.kind) {
        <p>{{ error.message }}</p>
      }
    }
  `,
})
export class PasswordFormComponent {
  readonly passwordModel = signal<PasswordModel>({
    password: '',
    confirmPassword: '',
  });

  readonly passwordForm = form(this.passwordModel, (path) => {
    required(path.password, { message: 'Password is required' });
    minLength(path.password, 8, { message: 'Use at least 8 characters' });
    required(path.confirmPassword, { message: 'Confirm your password' });

    validate(path.confirmPassword, ({ value, valueOf }) => {
      if (value() !== valueOf(path.password)) {
        return {
          kind: 'passwordMismatch',
          message: 'Passwords do not match',
        };
      }

      return null;
    });
  });
}
```

With Reactive Forms, cross-field validation often meant placing a validator on the group, then manually deciding which control should display the error. With Signal Forms, the validator can read another field through the form context and return a field-specific error.

That is a small API difference with a big maintenance impact.

### Async Validation Without the RxJS Maze

Async validation used to be one of those places where Angular developers quietly accepted a lot of RxJS boilerplate. You needed debounce behavior, cancellation, loading state, error mapping, and a clean way to avoid running the request when basic validation already failed.

Angular 22 gives Signal Forms a much clearer path:

```ts
import { Component, signal } from '@angular/core';
import { form, FormField, required, validateHttp } from '@angular/forms/signals';

type RegistrationModel = {
  username: string;
};

@Component({
  selector: 'app-registration',
  imports: [FormField],
  template: `
    <input [formField]="registrationForm.username" />

    @if (registrationForm.username().pending()) {
      <p>Checking availability...</p>
    }

    @for (error of registrationForm.username().errors(); track error.kind) {
      <p>{{ error.message }}</p>
    }
  `,
})
export class RegistrationComponent {
  readonly registrationModel = signal<RegistrationModel>({
    username: '',
  });

  readonly registrationForm = form(this.registrationModel, (path) => {
    required(path.username, { message: 'Username is required' });

    validateHttp(path.username, {
      request: ({ value }) => {
        const username = value().trim();
        return username ? `/api/users/check-name?username=${username}` : undefined;
      },
      onSuccess: (response: { available: boolean }) => {
        return response.available
          ? null
          : { kind: 'usernameTaken', message: 'That username is already taken' };
      },
      onError: () => ({
        kind: 'usernameCheckFailed',
        message: 'Could not check this username right now',
      }),
    });
  });
}
```

The async state is not somewhere else. `pending()`, `errors()`, and `invalid()` live beside the field.

### Dynamic Forms Become Easier to Reason About

Dynamic forms are where Angular teams usually start building custom abstractions. Think onboarding flows, survey products, admin-defined forms, insurance applications, and internal workflow tools.

With Signal Forms, the model can be generated from configuration, while the schema layer describes behavior.

```ts
type Question = {
  key: string;
  label: string;
  required?: boolean;
};

const questions = signal<Question[]>([
  { key: 'companyName', label: 'Company name', required: true },
  { key: 'website', label: 'Website' },
]);

const answerModel = signal<Record<string, string>>({
  companyName: '',
  website: '',
});

const answerForm = form(answerModel, (path) => {
  for (const question of questions()) {
    if (question.required) {
      required(path[question.key], {
        message: `${question.label} is required`,
      });
    }
  }
});
```

In a real app, you would wrap this in a form-builder layer, but the interesting part is the same: the state, validation, and UI can all react to the same model.

### How This Impacts Form Builders

Large form-builder products benefit from Signal Forms because form state becomes more granular and more explicit.

Imagine building a Typeform alternative. You care about conditional questions, step-level validation, partial autosave, async checks, mobile typing performance, and analytics around dirty or abandoned fields.

Reactive Forms can do all of this, but teams usually build a framework around the framework. Signal Forms reduce the amount of custom glue because each field is already a reactive unit.

For form-heavy SaaS apps, that is the real win. You are getting a better base for complex, configurable form systems.

## OnPush Is Now the Default

Angular 22 makes `OnPush` the default change detection strategy for new applications. The older broad checking behavior is now represented by `ChangeDetectionStrategy.Eager`.

Before Angular 22, a component without an explicit `changeDetection` setting used the old default strategy. Browser events, timers, XHRs, and other async work could cause Angular to check a wide part of the component tree.

That behavior was convenient, but it also allowed inefficient code to survive for a long time.

```ts
@Component({
  selector: 'app-orders-table',
  template: `<app-order-row *ngFor="let order of orders" [order]="order" />`,
})
export class OrdersTableComponent {
  orders: Order[] = [];
}
```

Under the older default model, mutating local fields often appeared to work because change detection was eager.

```ts
addOrder(order: Order) {
  this.orders.push(order);
}
```

With `OnPush`, you should treat this as a bug waiting to happen. Use immutable updates or signals:

```ts
readonly orders = signal<Order[]>([]);

addOrder(order: Order) {
  this.orders.update((current) => [...current, order]);
}
```

The template then reads the signal:

```html
@for (order of orders(); track order.id) {
  <app-order-row [order]="order" />
}
```

This is the Angular 22 mindset: make the changed state visible to Angular through signals, inputs, events, or explicit change detection APIs.

### Migration Considerations

The biggest upgrade risk is not compilation. It is behavior.

Places to inspect:

* Components that mutate arrays or objects in place.
* Manual subscriptions that assign to plain class fields.
* Third-party callbacks that run outside Angular awareness.
* Deep component trees where child components expect broad parent checks.
* Tests that pass because `fixture.detectChanges()` hides missing reactive wiring.

If a component really needs the old behavior during migration, Angular 22 gives you a clearer name:

```ts
import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
  selector: 'app-legacy-grid',
  changeDetection: ChangeDetectionStrategy.Eager,
  template: `...`,
})
export class LegacyGridComponent {}
```

Use that as a temporary bridge, not a permanent hiding place.

### Where OnPush Helps Most

The performance gains show up most clearly in large UIs: grids, dashboards, reporting tools, admin screens, and enterprise routes with several feature areas.

The benefit is not magic speed. Angular checks less code by default, and your state updates become easier to trace.

## Resource APIs Are Stable

Angular 22 also stabilizes the asynchronous reactivity APIs: `resource()`, `rxResource()`, and `httpResource()`.

These APIs do not replace RxJS everywhere. They replace the common "fetch data and show loading, value, and error state" pattern that many Angular apps have repeated for years.

Here is the traditional shape:

```ts
readonly query = new FormControl('');
readonly results$ = this.query.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap((query) =>
    this.searchService.search(query ?? '').pipe(
      map((items) => ({ status: 'success' as const, items })),
      startWith({ status: 'loading' as const, items: [] }),
      catchError((error) => of({ status: 'error' as const, error, items: [] }))
    )
  )
);
```

That code is fine if your team is strong with RxJS. But for a simple search box, it is a lot of structure.

With `httpResource()`, the request can follow signals directly:

```ts
import { Component, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';

@Component({
  selector: 'app-product-search',
  template: `
    <input
      [value]="query()"
      (input)="query.set($any($event.target).value)"
      placeholder="Search products"
    />

    @if (products.isLoading()) {
      <p>Loading...</p>
    } @else if (products.error()) {
      <p>Could not load products.</p>
    } @else {
      @for (product of products.value(); track product.id) {
        <article>{{ product.name }}</article>
      }
    }
  `,
})
export class ProductSearchComponent {
  readonly query = signal('');

  readonly products = httpResource<Product[]>(() => {
    const q = this.query().trim();
    return q ? `/api/products?q=${encodeURIComponent(q)}` : undefined;
  });
}
```

The value, loading state, and error state are all readable from the resource. When `query()` changes, the request can rerun.

### Pagination Example

Resources also fit paginated screens nicely:

```ts
readonly page = signal(1);
readonly pageSize = signal(25);

readonly invoices = httpResource<{
  items: Invoice[];
  total: number;
}>(() => ({
  url: '/api/invoices',
  params: {
    page: this.page(),
    pageSize: this.pageSize(),
  },
}));

nextPage() {
  this.page.update((page) => page + 1);
}
```

This gives table components a clean model: the current page is a signal, the resource depends on it, and the template reads one consistent object.

### Traditional Angular Service Pattern vs Resource API

| Concern        | Traditional service plus RxJS                                     | Resource API                  |
| -------------- | ----------------------------------------------------------------- | ----------------------------- |
| Loading state  | Usually custom `BehaviorSubject`, `startWith`, or component state | Built into the resource       |
| Error state    | Usually `catchError` plus custom shape                            | Built into the resource       |
| Refetching     | Manual subscription or stream trigger                             | Recomputed from signal params |
| Template usage | `async` pipe or conversion to signals                             | Direct signal reads           |
| Best for       | Complex streams, websockets, event pipelines                      | Request-driven UI state       |

RxJS still belongs in Angular. If you are modeling websockets, event streams, cancellation-heavy workflows, or multi-source composition, RxJS remains excellent.

But if you are fetching a list of products because a search signal changed, a resource is often the cleaner tool.

## HttpClient and Fetch

Angular 22 continues the move from `XMLHttpRequest` toward Fetch for `HttpClient`.

This matters because Fetch is the modern browser primitive. It works better with streaming, request and response primitives, abort signals, and server environments. It also makes Angular's client and SSR behavior easier to align over time.

For most app code, your `HttpClient` calls still look familiar:

```ts
this.http.get<User[]>('/api/users');
```

The difference is under the backend.

Fetch changes a few details teams should test:

* Progress events are not identical to XHR progress events.
* Upload progress needs special attention.
* Streaming responses become more natural.
* Abort behavior maps to platform APIs.
* SSR request handling becomes easier to reason about.

If your application depends on XHR-specific behavior, test those flows carefully during the upgrade. File uploads, progress bars, custom interceptors, auth redirects, and transfer cache behavior deserve real QA.

## The New @Service Decorator

Angular 22 introduces `@Service()` as a shorter way to define common application services.

The old pattern is still valid:

```ts
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class UserPreferencesService {}
```

The new pattern is clearer for the common singleton case:

```ts
import { Service } from '@angular/core';

@Service()
export class UserPreferencesService {
  private readonly theme = signal<'light' | 'dark' | 'system'>('system');

  currentTheme = this.theme.asReadonly();

  setTheme(theme: 'light' | 'dark' | 'system') {
    this.theme.set(theme);
  }
}
```

Use `@Service()` when you want the standard root-provided service shape. Keep `@Injectable()` when you need deeper provider configuration, special DI behavior, or a pattern already used heavily in an existing library.

This is not a huge architectural feature. It is a readability feature. But Angular has carried a lot of ceremony for years, so readability changes still matter.

## injectAsync()

`injectAsync()` solves a real performance problem: services could be injected eagerly even when they were only needed for rare actions.

Think about exports, PDF generation, chart rendering, image processing, or a large editor integration. You do not want every user to pay for that code on initial load.

```ts
import { Component, injectAsync, onIdle } from '@angular/core';

@Component({
  selector: 'app-report-actions',
  template: `<button (click)="exportCsv()">Export CSV</button>`,
})
export class ReportActionsComponent {
  private readonly exporter = injectAsync(
    () => import('./csv-exporter').then((m) => m.CsvExporterService),
    { prefetch: onIdle }
  );

  async exportCsv() {
    const exporter = await this.exporter();
    await exporter.export();
  }
}
```

The service must be auto-provided, either with `@Service()` or `@Injectable({ providedIn: 'root' })`.

Use `injectAsync()` for heavy, optional services: exports, editors, admin-only analytics tools, SDK wrappers, and expensive visualization helpers.

Do not use it for tiny services that every route needs anyway. Auth state, router helpers, formatting services, and startup dependencies should usually stay eagerly available. Lazy loading the wrong dependency can make code harder to follow and can add a delay right when the user clicks.

## Incremental Hydration Improvements

Hydration is the process of taking server-rendered HTML and connecting it to client-side Angular so the page becomes interactive.

The hard part is that full hydration can force the browser to download and execute more JavaScript than the first interaction needs. That hurts pages with lots of below-the-fold widgets, dashboards, comments, ads, recommendations, maps, charts, or product configurators.

Incremental hydration lets Angular hydrate only the parts that need to become interactive. Combined with `@defer`, it gives teams a more practical SSR model:

```html
<article>
  <h1>{{ post.title }}</h1>
  <p>{{ post.summary }}</p>
</article>

@defer (on viewport) {
  <app-comments [postId]="post.id" />
} @placeholder {
  <p>Comments loading...</p>
}
```

The server can still send meaningful HTML early. The browser can delay work for parts of the page that are not needed yet.

This matters for SEO because crawlers and social previews can see useful content in the initial HTML. It matters for users because the first meaningful view does not need to wait for every interactive island.

Real-world use cases:

* Blog posts with interactive examples.
* Product pages with reviews and recommendations.
* Dashboards with below-the-fold charts.
* Documentation pages with demos.
* Ecommerce pages with expensive personalization widgets.

Angular 22 does not mean you can ignore hydration design. You still need to decide which parts of a route should be interactive immediately. But the framework is giving you better defaults and cleaner tools.

## Angular Aria Is Stable

Angular Aria gives Angular developers headless accessible primitives for common UI patterns.

This is important because accessibility is not only about adding `aria-label` at the end. Complex widgets need keyboard behavior, focus management, roles, states, and screen reader expectations that match WAI-ARIA patterns.

For example, a toolbar is not just a row of buttons. It needs predictable keyboard navigation:

```ts
import { Component } from '@angular/core';
import { Toolbar, ToolbarWidget } from '@angular/aria/toolbar';

@Component({
  selector: 'app-editor-toolbar',
  imports: [Toolbar, ToolbarWidget],
  template: `
    <div toolbar aria-label="Editor formatting">
      <button toolbarWidget type="button">Bold</button>
      <button toolbarWidget type="button">Italic</button>
      <button toolbarWidget type="button">Underline</button>
    </div>
  `,
})
export class EditorToolbarComponent {}
```

The value is that your design system can stay visually custom while the behavior is backed by tested accessibility primitives.

Angular Aria is especially useful for:

* Menus.
* Toolbars.
* Tabs.
* Trees.
* Listboxes.
* Custom selects.
* Combobox-style controls.

If your team maintains a component library, Angular Aria deserves a serious look before building another custom keyboard-navigation layer from scratch.

## Template Improvements

Angular 22 includes several template improvements that make templates easier to write and easier to type-check.

### Comments Inside HTML Elements

You can now add comments around attributes and bindings:

```html
<button
  // Kept explicit because analytics depends on this stable id.
  data-event-id="checkout-submit"
  [disabled]="checkoutForm().invalid()"
>
  Place order
</button>
```

This is helpful when a binding exists for a non-obvious reason.

### Spread Syntax

Templates can use spread syntax in object literals, array literals, and function calls:

```html
<app-product-card
  [metadata]="{ ...baseMetadata, source: 'search-results' }"
  [tags]="[...product.tags, 'recommended']"
/>
```

Keep it readable. If the expression starts looking like business logic, move it to the component.

### Better @switch

Multiple cases can share one block:

```html
@switch (order.status) {
  @case ('pending')
  @case ('processing') {
    <app-status-badge tone="info">In progress</app-status-badge>
  }
  @case ('shipped') {
    <app-status-badge tone="success">Shipped</app-status-badge>
  }
  @default {
    <app-status-badge tone="muted">Unknown</app-status-badge>
  }
}
```

Angular 22 also improves exhaustive checking for `@switch`, which is useful when the switch expression is a union type.

### Arrow Functions in Templates

Small inline arrow functions are now valid:

```html
<button (click)="cart.update((items) => [...items, product()])">
  Add to cart
</button>
```

Use this carefully. A short signal update is fine. A multi-step pricing calculation belongs in TypeScript.

## Security Improvements

Angular 22 includes practical security hardening across template sanitization, SVG handling, URL handling, and SSR behavior.

The changes worth noticing:

* Dynamic `href` and `xlink:href` bindings inside SVG are sanitized more carefully.
* URL validation is stricter in server-side code paths.
* SSR hardening reduces risk around suspicious URLs and request handling.
* Transfer cache behavior is safer for requests that may include credentials or cookies.

Why should application developers care? Because modern Angular apps often render icons, SVGs, CMS fields, markdown, rich text, embedded links, and server-rendered pages. A small sanitization gap in those areas can become a real security issue.

You still need to avoid unsafe bypasses. Treat `DomSanitizer.bypassSecurityTrust...` methods as review-worthy code. Angular can protect a lot, but it cannot save an app that tells it to trust untrusted content.

## What Angular Developers Should Learn First

### Learn Immediately

**Signal Forms** because every new form should be evaluated against the stable signal-based API.

**Resource APIs** because most apps fetch data everywhere, and `httpResource()` can remove a lot of repeated loading and error state.

**OnPush mindset** because new code should prefer signals, immutable updates, and visible state changes.

### Learn Next

**Fetch-based HttpClient** if you have upload progress, interceptors, SSR, or transfer cache behavior.

**Incremental Hydration** if you build public pages, ecommerce flows, docs, or content-heavy apps.

**injectAsync** when you have heavy optional services that should not be part of the first load.

### Nice To Know

**@Service** is useful and readable, but not urgent.

**Angular Aria** is critical if you own a design system, less urgent if your app already uses a mature component library.

**Template improvements** are worth learning gradually. Use them to make templates clearer, not more clever.

## What This Means for Existing Angular Applications

For existing apps, the Angular 22 upgrade is less about one huge breaking change and more about surfacing old assumptions.

| Current version | Main concern                                                                                     | Best first move                                         |
| --------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
| Angular 17      | Several eras change at once: control flow, standalone patterns, signals, hydration, and defaults | Stabilize the framework upgrade before rewriting forms  |
| Angular 18      | Mixed signal and non-signal code                                                                 | Convert shared UI and new feature work first            |
| Angular 19      | Async state may still be custom RxJS scaffolding everywhere                                      | Try resources on one list or detail page                |
| Angular 20      | Defaults matter more than syntax                                                                 | Audit `OnPush`, Fetch, SSR, and transfer cache behavior |
| Angular 21      | Early Signal Forms or resources may differ from final APIs                                       | Compare against Angular 22 docs and remove workarounds  |

The safest upgrade path is boring in a good way. Run the official update, keep compatibility migrations where needed, then test the screens where eager checking hid mistakes.

Pay special attention to forms, lazy routes, SSR pages, upload flows, manual subscriptions, third-party components, large tables, and dashboards. Those are the places where small framework changes are most likely to reveal real app assumptions.

## Angular 22 for Enterprise Applications

Angular 22 is especially relevant for enterprise apps because enterprise UIs are where the old defaults usually hurt first.

Large datasets need predictable rendering, so `OnPush`, signals, and immutable updates help tables, grids, and dashboards avoid unnecessary checks. Reporting apps need request state everywhere, which makes resources a useful shared pattern. Internal tools and SaaS products are often form-heavy, so Signal Forms help keep conditional validation and cross-field logic close to the model.

Public SaaS pages also benefit from SSR and hydration work because useful HTML can arrive early while secondary widgets wait until they are needed.

For large apps, the strategy is simple:

* Start new forms with Signal Forms.
* Consider resources for new request-driven screens.
* Write new components with the `OnPush` mindset.
* Use `injectAsync()` for heavy optional services.
* Use Angular Aria before hand-writing complex keyboard behavior.

That is how teams modernize without turning the release into a rewrite project.

## Final Thoughts

Angular 22 makes Angular's future much easier to read.

Signals are no longer a side feature. They are becoming the language Angular uses for state, forms, async work, and rendering.

RxJS is not going away, and it should not. But Angular is reducing the number of places where developers needed RxJS only because the framework had no simpler model.

The best learning investment for the next 6 to 12 months is straightforward:

1. Learn Signal Forms well enough to build one real production form.
2. Learn `httpResource()` well enough to replace one list page's loading and error state.
3. Audit your mutation habits for `OnPush`.
4. Understand Fetch-related behavior if your app does uploads, SSR, or transfer cache.
5. Study Angular Aria if your team owns custom UI primitives.

Angular 22 is not asking experienced Angular teams to forget everything they know. It is asking them to stop carrying old patterns into new code when the framework now has better defaults.

## Sources

* [Official Angular v22 release announcement](https://blog.angular.dev/announcing-angular-v22-c52bb83a4664)
* [Angular v22 release page](https://angular.dev/events/v22)
* [Angular v22 changelog](https://github.com/angular/angular/releases/tag/22.0.0)
* [Signal Forms guide](https://angular.dev/guide/forms/signals/validation)
* [httpResource guide](https://angular.dev/guide/http/http-resource)
* [Lazy loading services with injectAsync](https://angular.dev/guide/di/lazy-loading-services)
* [Angular Aria overview](https://angular.dev/guide/aria/overview)


Last updated on June 7, 2026

---
title: "Have you ever thought about how platforms implement integrations?"
description: "I went down the rabbit hole building Pabble's integration system. Here's what I figured out, the Provider Registry Pattern explained simply, no CS degree required."
last_updated: "May 18, 2026"
source: "https://harshkanjiya.com/blog/how-integrations-work"
---

# Have you ever thought about how platforms implement integrations?

I went down the rabbit hole building Pabble's integration system. Here's what I figured out, the Provider Registry Pattern explained simply, no CS degree required.

I thought so too. And here's what I ended up building.

***

Okay, real talk.

When I started building [Pabble](https://pabble.app) \[a form workflow tool for freelancers and agencies]. I hit a wall pretty early. Not a bug. Not a design problem. Just a question that kept nagging me at 1am:

> *"When my users want to connect Slack, or HubSpot, or Google Sheets… how the heck does that actually work under the hood?"*

I went down the rabbit hole. I read docs. I stared at other tools. I drew a lot of messy diagrams on my iPad.

And then it clicked.

Here's everything I figured out, explained simply, no computer science degree required.

***

## The bad way first (so you appreciate the good way)

Imagine you're building integrations the naive way. User wants Slack? You write Slack code directly into your app. User wants HubSpot? You write HubSpot code right next to the Slack code. User wants Mailchimp? You add it right there too.

Six months later, your codebase looks like a plate of spaghetti that someone sneezed on.

Adding integration #12 means touching files that have nothing to do with integrations. A bug in Slack somehow breaks HubSpot. You dread every new integration request.

There had to be a better way.

***

## The idea that changed everything: the plug-in socket

Think about a power strip at home.

You don't rewire your house every time you want to plug in a new lamp. The socket has a standard shape. Any device that fits the shape just… works. The house doesn't care if it's a lamp or a phone charger or a toaster.

Integrations can work the exact same way.

What if every integration (Slack, Google Sheets, HubSpot, whatever) was just a "plug" that fit the same standard socket? And the rest of Pabble never had to care *which* plug was connected, just that *something* was connected?

That's the insight. And it has a fancy name: the **Provider Registry Pattern**.

But don't let the name scare you. It's just a power strip for your code.

***

## How it actually works

Here's the system I built for Pabble, broken into four layers. Think of it like a building.

### Floor 4: The Workflow Engine (the people upstairs)

This is where your workflow lives. The eight step types: **Collect, Notify, Approve, Wait, Branch, Generate, Charge, Connect.**

When a workflow runs and hits a "Connect" step (notify Slack when this form is submitted) it doesn't know or care what Slack is. It just says:

> *"Hey, run integration ID `slack` with this payload."*

That's it. It passes the data and moves on.

***

### Floor 3: The Integration Bus (the coordinator)

This floor handles the messy stuff your workflow engine shouldn't have to think about:

* **Event queue**: makes sure the right integration fires at the right moment
* **Credential store**: safely holds the user's API keys and OAuth tokens so Pabble never pays a penny for external services (the user connects their own account)
* **Retry + audit log**: if Slack is down for 30 seconds, it retries automatically and logs everything

The workflow engine drops a message here. This floor figures out the rest.

***

### Floor 2 - The Provider Registry (the directory)

This is the magic floor.

Every integration lives here as its own self-contained file. Each file - called an **adapter** - looks like this:

```typescript
// integrations/adapters/slack.ts

export const SlackAdapter = {
  id: "slack",
  name: "Slack",
  authType: "oauth2",

  async execute(action, context) {
    await fetch("https://slack.com/api/chat.postMessage", {
      method: "POST",
      headers: { Authorization: `Bearer ${context.credentials.accessToken}` },
      body: JSON.stringify({
        channel: action.params.channel,
        text: action.params.text,
      }),
    });
    return { success: true };
  },
};
```

Every adapter speaks the same language. Same structure. Same method names. Like every plug fitting the same socket.

The registry is just a simple list:

```typescript
// integrations/registry.ts

import { SlackAdapter } from "./adapters/slack";
import { SheetsAdapter } from "./adapters/sheets";
import { HubSpotAdapter } from "./adapters/hubspot";
// ... more here

export const registry = {
  slack: SlackAdapter,
  sheets: SheetsAdapter,
  hubspot: HubSpotAdapter,
};
```

Want to add Microsoft Teams next month? Write one file. Add one line to this list. Ship it.

*Nothing else changes. Not one other file.*

***

### Floor 1 - The Auth Layer (the security desk)

Before any integration fires, the user's credentials need to be fetched and verified. This floor handles three types:

* **OAuth2** - the "Login with Google/Slack/HubSpot" flow
* **API key** - user pastes their key, Pabble stores it encrypted
* **Webhook** - Pabble sends a POST to whatever URL the user provides

The key insight here? **Users bring their own credentials.** Pabble never signs up for a Slack account on their behalf. Users connect their own workspace. Which means Pabble pays exactly $0 for any integration at the platform level.

(Except Stripe, which takes a cut of transactions - but that's unavoidable and the user expects it anyway.)

***

## The file structure (the beautiful part)

Once everything clicks, the folder structure basically designs itself:

```
/integrations
  registry.ts           ← the only file that imports adapters
  base.types.ts         ← the standard "socket" shape every adapter must fit
  /adapters
    slack.ts
    google-sheets.ts
    hubspot.ts
    mailchimp.ts
    calendly.ts
    zapier.ts
    airtable.ts
    notion.ts
    activecampaign.ts
    pipedrive.ts
    google-analytics.ts
    microsoft-teams.ts
    ← drop new files here. forever.
```

It's so clean it almost makes me emotional.

***

## The "aha" moment

Here's the thing I kept getting wrong before I figured this out:

I was thinking about integrations as *features*.

But they're not features. They're **plug-ins**.

A feature lives inside your product. A plug-in lives at the edge of your product, connected to the outside world. When you treat them differently - give them their own dedicated layer, their own interface, their own isolated files - everything gets easier.

Adding integration #20 takes the same amount of effort as adding integration #2.

A bug in Notion never touches Slack.

A new engineer on the team can write a new adapter in an afternoon without touching anything they shouldn't.

***

## What I'm building next

Pabble is currently focused on **client onboarding workflows** for freelancers and agencies - the boring, painful, repetitive stuff like collecting briefs, getting approvals, sending contracts, and chasing payments.

The first integrations I'm shipping (in order of usefulness):

1. **Slack** - notify your team the second a client submits something
2. **Google Sheets** - auto-log every response, freelancers live in Sheets
3. **HubSpot** - push onboarded clients straight into your CRM
4. **Mailchimp** - trigger email sequences after workflow completion
5. **Calendly** - embed a booking step inside the onboarding flow
6. **Zapier** - the escape hatch for everything not natively supported
7. **Airtable** - structured client database for agencies managing many clients
8. **Notion** - many freelancers use Notion as their client hub
9. **ActiveCampaign** - email automations for the power users
10. **Pipedrive** - for sales-focused freelancers who live in their pipeline
11. **Google Analytics** - track workflow completion and drop-off
12. **Microsoft Teams** - for the agencies on the Microsoft stack

And the beauty of the system? When someone asks me to add integration #13, #20, or #47 - I just write one file, add one line, and ship.

***

## TL;DR

* Don't write integrations directly into your app - you'll regret it
* Use the **Provider Registry Pattern**: each integration is a self-contained adapter file with a standard interface
* A central registry maps IDs to adapters - the rest of your app never imports integrations directly
* Users bring their own credentials - Pabble pays $0 per integration
* Adding new integrations forever = write one file, add one line

***

If you're building something similar and want to nerd out about architecture, I'm always up for it. Hit me up.

And if you're a freelancer or agency drowning in client onboarding chaos - [Pabble](https://pabble.app) is being built for you. Early access is open.

***

*- Harsh, building Pabble*


Last updated on May 18, 2026

---
title: "CAP Theorem in System Design"
description: "A clear, practical explanation of the CAP Theorem and its implications for building distributed systems."
last_updated: "January 26, 2026"
source: "https://harshkanjiya.com/blog/cap-theorem-in-system-design"
---

# CAP Theorem in System Design

A clear, practical explanation of the CAP Theorem and its implications for building distributed systems.

The **CAP Theorem** is a foundational concept in distributed systems design, often discussed in system design interviews and real-world architecture decisions. It explains why distributed systems must make trade-offs between key properties when failures occur.

## What is the CAP Theorem?

In a distributed system, **CAP** stands for:

* **Consistency** – Every read returns the *most recent write* or an error.
* **Availability** – Every request receives *a response* (success or failure) without guaranteed freshness.
* **Partition Tolerance** – The system continues to operate even when network partitions (failures in communication between nodes) occur.

The CAP Theorem states that **in the presence of a network partition, a distributed system can only guarantee at most two of these three properties simultaneously**. You cannot fully achieve all three at the same time.

## Why CAP Matters

Distributed systems run across multiple machines or data centers. Network issues such as dropped messages or delayed communication are inevitable in real environments. The CAP Theorem shows that when such *partitions* happen, a system must choose:

* **Consistency**: make sure data is correct everywhere, even if some parts stop responding.
* **Availability**: always respond to client requests, even if some responses may be outdated.

This trade-off influences how systems behave under failure and how users experience data correctness and availability.

***

## The Three Properties in Detail

### Consistency

Consistency in CAP means:

> After a write completes, *all future reads* will return that write from *every node*.

This is stronger than typical ACID consistency — it’s about *global visibility* of the latest data across every node. Strong consistency ensures correctness but can reduce availability when parts of the network fail.

***

### Availability

Availability means the system will *always respond* to client requests, even if some parts of the system are unreachable.

However, an available response doesn’t guarantee that the data is up-to-date when partitions occur. Some nodes may return stale data.

***

### Partition Tolerance

Partition tolerance is the system’s ability to continue functioning even when communication breaks between nodes due to network faults.

In practice, partition tolerance isn’t optional — network failures happen — so real-world distributed systems almost always have to tolerate partitions.

***

## CAP Trade-Offs

When a network partition occurs, a system must *compromise* one property:

### CP: Consistency + Partition Tolerance

* Ensures correct, synchronized data across nodes.
* May *sacrifice availability* by rejecting requests during partitions.

> Example: Banking systems where data accuracy matters even if some requests are delayed.

***

### AP: Availability + Partition Tolerance

* Always responds to requests.
* May *serve stale or inconsistent data* during partitions.

> Example: Social media feeds where responsiveness matters more than immediate global consistency.

***

### CA: Consistency + Availability

* In theory, serves up-to-date data and responds to requests.
* **Not realistic in distributed systems with partitions**, because partition tolerance can’t be ignored.


Last updated on January 26, 2026

---
title: "Signal Forms: Last Forms API You'll Ever Need"
description: "Angular 21 introduces Signal Forms, a ground-up rewrite that ditches Zone.js and RxJS overhead for fine-grained reactivity."
last_updated: "December 1, 2025"
source: "https://harshkanjiya.com/blog/signal-forms-in-angular-21"
---

# Signal Forms: Last Forms API You'll Ever Need

Angular 21 introduces Signal Forms, a ground-up rewrite that ditches Zone.js and RxJS overhead for fine-grained reactivity.

For a decade, Angular forms felt like a compromise. You either chose **Template-Driven** (easy but untestable) or **Reactive** (powerful but bloated with RxJS boilerplate). Angular 21 finally kills the "lesser of two evils" debate by introducing **Signal Forms**.

This isn't just a new API; it’s a complete architectural pivot toward a **Zoneless** future.

***

## The "Observable" Tax

Before we look at the new hotness, let’s be honest about why Reactive Forms are frustrating:

* **Change Detection Overhead:** They rely on Zone.js. Every keystroke triggers a global "check everything" cycle.
* **Stream Fatigue:** We often didn't need a full RxJS stream just to toggle a button state based on a checkbox.
* **Boilerplate:** `FormGroup` and `ControlValueAccessor` required too much ceremony for simple data entry.

***

## The Signal Paradigm: State over Streams

In Angular 21, every field is a `WritableSignal`. The form itself is a `computed` signal. No subscriptions, no manual cleanup, and no `takeUntilDestroyed()`.

### 1. Zero-Boilerplate Setup

Forget the `FormBuilder` service. You define your structure directly as a reactive object using `signalForm()`.

```ts
const form = signalForm({
  email: sfField("", { validators: [sfValidator.required, sfValidator.email] }),
  password: sfField(""),
});
```

### 2. Native Async Power

Handling async validation used to require RxJS pipe-chains for debouncing. Now, it’s a configuration property.

```ts
const username = sfField("", {
  asyncValidators: [(val) => checkUnique(val)],
  asyncDebounce: 400, // Native debouncing, no RxJS needed.
});
```

***

## Performance: The Zoneless Edge

This is the most critical win. In a **Zoneless** app (`provideZonelessApp()`), Signal Forms are a superpower.

* **Reactive Forms:** Keystroke -> Zone.js intercepts -> Global CD runs -> Entire tree re-renders.
* **Signal Forms:** Keystroke -> Signal updates -> **Only the affected UI nodes update.** This is how you build 60fps forms on low-end mobile devices and drop 70kb of Zone.js from your bundle.

### Testing without the Headache

Testing Reactive forms often meant mocking the entire `FormsModule`. Testing Signal forms is just testing plain logic.

```ts
it("should invalidate empty fields", () => {
  const form = signalForm({
    name: sfField("", { validators: [sfValidator.required] }),
  });

  expect(form.valid()).toBe(false);

  form.name.value.set("Developer");
  expect(form.valid()).toBe(true);
});
```

No `fakeAsync`, no `tick()`. Just set the value and assert the state.

***

## Advanced: Cross-Field Validation

The classic "Confirm Password" problem is finally solved beautifully. Since validators can just read other signals, cross-field logic is just a function.

```ts
const authForm = signalForm(
  {
    pass: sfField(""),
    confirm: sfField(""),
  },
  {
    groupValidators: [
      (fields) => {
        return fields.pass.value() === fields.confirm.value()
          ? null
          : { message: "Mismatch", field: "confirm" };
      },
    ],
  },
);
```

***

## Final Verdict

Signal Forms represent the unification of the Angular developer experience. State, UI, and Forms now speak the exact same language: **Signals.** If you're starting a project today, use the official schematic to go all-in:

```bash
ng generate @angular/forms:migrate-to-signal-forms
```

The transition from "Streams" to "State" is the best thing to happen to Angular in years. It’s time to stop managing subscriptions and start building features.


Last updated on December 1, 2025

---
title: "Angular 21: Pure Developer Flow"
description: "Zone.js is dead, partial hydration is standard, and templates are cleaner than ever. Breaking down the Angular 21 renaissance."
last_updated: "November 25, 2025"
source: "https://harshkanjiya.com/blog/angular-21-overview"
---

# Angular 21: Pure Developer Flow

Zone.js is dead, partial hydration is standard, and templates are cleaner than ever. Breaking down the Angular 21 renaissance.

For years, writing Angular felt like entering a corporate boardroom. You had to bring your modules, your services, your decorators, and your `rxjs` subscriptions just to get a seat at the table.

But over the last few major versions, the Angular team has been quietly tearing down the walls. With the release of **Angular 21**, the renovation is complete. The framework has finally gotten out of its own way.

Here are the biggest architectural shifts in v21 and why they change how we build web apps.

***

## 1. The Death of Zone.js (Zoneless is Stable)

We’ve been talking about "Zoneless Angular" since v18, but Angular 21 marks the milestone where it is officially the default recommendation.

Zone.js was a magic trick that patched browser APIs to figure out when your state *might* have changed. It worked, but it was heavy (\~100kb uncompressed) and caused massive performance bottlenecks on large DOMs.

Now, by bootstrapping with `provideZonelessChangeDetection()`, Angular relies entirely on **Signals**.

* **The Result:** Change detection is now localized. If a signal changes deep in a child component, Angular updates exactly that DOM node—without checking the entire component tree. Your bundle size drops, and your app feels instantly snappier.

***

## 2. Partial Hydration: SSR that Makes Sense

Server-Side Rendering (SSR) used to be an all-or-nothing game. The server sent the HTML, and the client had to download all the JavaScript and "rehydrate" the entire page before anything became interactive.

Angular 21 introduces **Partial Hydration** out of the box, tied directly to `@defer` blocks.

```html
@defer (on interaction) {
<complex-interactive-widget />
} @placeholder {
<button>Load Widget</button>
}
```

Now, the server renders the HTML for the placeholder. The JavaScript for the `complex-interactive-widget` isn't even downloaded or executed until the user actually interacts with it. This is a massive win for initial load times and Core Web Vitals, mimicking the "island architecture" popularized by frameworks like Astro.

***

## 3. The `@let` Syntax Matures

We finally have local variables in templates. No more weird `*ngIf="obs$ | async as data"` hacks just to hold onto a value.

With the `@let` block, templates look like modern, minimalist code:

```html
@let taxRate = 0.15; @let totalPrice = basePrice() * (1 + taxRate);

<div class="checkout-panel">
  <h2>Total: {{ totalPrice | currency }}</h2>
</div>
```

It sounds small, but removing business logic gymnastics from the HTML makes UI development significantly cleaner.

***

## 4. Signal Forms (The 3rd Paradigm)

As I covered in my previous deep-dive, Angular 21 ships with a complete rewrite of the Forms API.

We can finally ditch the heavy RxJS Observables of Reactive Forms in favor of pure, type-safe `WritableSignals`. If you are still using `FormBuilder` and `ControlValueAccessor`, it's time to upgrade. The boilerplate reduction alone is worth the refactor.

***

## 5. NgModules are a Relic

While technically deprecated a while ago, Angular 21 effectively wipes `NgModule` from the schematics. Standalone components aren't just an option anymore; they are the ecosystem standard. If you are starting a new architecture today, you don't need a single `.module.ts` file.

Everything is imported directly where it is used, making dependency tracking incredibly straightforward.

***

## The Verdict

Angular 21 isn't about adding complex new APIs; it's about **removing the mental overhead**.

By standardizing Signals, dropping Zone.js, and natively supporting partial hydration, Angular now offers a modern, minimalist developer experience that rivals Vue and Svelte, but with the enterprise-grade architecture we've always relied on.

**Upgrading?** Run `ng update @angular/core@21 @angular/cli@21` and welcome to the clean side of the framework.


Last updated on November 25, 2025

---
title: "Quick UX Wins"
description: "This guide focuses on simple, high-leverage UX improvements inspired by Vercel’s product design principles. These are changes you can apply in minutes—cleaner focus states, better input behaviors, clearer interactions, safer defaults, and small accessibility fixes. Even without advanced skills or refactoring, these low-effort updates noticeably improve usability and perceived quality."
last_updated: "November 15, 2025"
source: "https://harshkanjiya.com/blog/quick-ux-wins"
---

# Quick UX Wins

This guide focuses on simple, high-leverage UX improvements inspired by Vercel’s product design principles. These are changes you can apply in minutes—cleaner focus states, better input behaviors, clearer interactions, safer defaults, and small accessibility fixes. Even without advanced skills or refactoring, these low-effort updates noticeably improve usability and perceived quality.

# <span className="mt-4 block">easy — quick ux wins</span>

Below is the **easy-level checklist** based on Vercel’s UX guidance. Each item takes **2–5 minutes** to implement and provides a quick, noticeable improvement. The reference for all guidelines is [Vercel’s design principles](https://vercel.com/design/guidelines).

## interactions

### Scale your buttons

<ScaleButtonDemo />

One easy way to make your interface feel instantly more responsive is to add a subtle scale down effect when a button is pressed. A scale of 0.97 on the :active pseudo-class should do the job

\
**Why it matters:** It provides immediate tactile feedback, making interactions feel
faster and more satisfying.

***

### loading buttons keep their label

<LoadingButtonDemo />

Show a spinner while maintaining the original button label.

\
**Why it matters:** Prevents layout shift and lets users know which action is in
progress without losing context.

***

### no dead zones

<NoDeadZoneDemo />

Make the entire visible area of a control interactive.

\
**Why it matters:** Users shouldn’t guess where to tap; a larger clickable area prevents
frustration and improves usability.

***

### clear focus states

Make keyboard navigation obvious by using `:focus-visible` for single controls and `:focus-within` for grouped controls.

\
**Why it matters:** Users navigating via keyboard or assistive devices can clearly
see where focus is, improving accessibility and reducing confusion.

***

### larger, forgiving hit targets

Ensure every clickable element has a hit area of **at least 24px**, even if the visual icon is smaller.

\
**Why it matters:** Small targets frustrate users, especially on mobile. Expanding
the touch area improves interaction reliability.

***

### mobile-friendly input sizing

Set mobile `<input>` font-size to **16px or larger**.

\
**Why it matters:** Prevents iOS Safari auto-zoom when inputs are focused, ensuring
layout stability and a smoother typing experience.

***

### respect browser zoom

Never disable pinch-zoom or viewport scaling.

\
**Why it matters:** Users with low vision or specific accessibility needs rely on
zooming. Disabling it can harm accessibility.

***

### allow paste everywhere

Never block pasting into `<input>` or `<textarea>` fields.

\
**Why it matters:** Blocking paste interrupts users, slows workflows, and can break
accessibility.

***

### use ellipsis correctly

For follow-up or loading actions, use an ellipsis: `Rename…`, `Saving…`.

\
**Why it matters:** Communicates that an action is ongoing or continues, creating
a familiar UX pattern.

***

### prevent double-tap zoom

Add `touch-action: manipulation;` to interactive elements on mobile.

\
**Why it matters:** Stops accidental zooming during taps, making controls feel more
responsive.

***

### customized tap highlight

Set `-webkit-tap-highlight-color` to match your theme.

\
**Why it matters:** Ensures the touch highlight aligns with your design, avoiding
jarring visual feedback on mobile.

***

### use real links for navigation

Always use `<a>` or `<Link>` for navigation instead of buttons or divs.

\
**Why it matters:** Maintains standard browser behaviors like middle-click, Cmd/Ctrl+Click,
and open-in-new-tab functionality.

***

### controlled overscroll

Use `overscroll-behavior: contain;` in modals or drawers.

\
**Why it matters:** Stops the background page from scrolling when users interact
with overlay content, keeping interactions predictable.

***

### autofocus where it makes sense

Enable autofocus on desktop when there’s a single primary input. Avoid on mobile.

\
**Why it matters:** Improves efficiency for desktop users without causing layout
shift or unexpected keyboard opening on mobile.

***

## animations

### respect prefers-reduced-motion

Provide reduced-motion variants for users who prefer minimal animation.

\
**Why it matters:** Supports accessibility preferences and reduces motion sickness
or distraction.

***

### prefer css-based animations

Use CSS transitions instead of JavaScript for simple motion.

\
**Why it matters:** CSS animations are smoother and less likely to block the main
thread.

***

### avoid `transition: all`

List only the properties you want to animate, such as `opacity` or `transform`.

\
**Why it matters:** Avoids unintended layout recalculations that can cause jank.

***

### compositor-friendly motion

Animate only GPU-accelerated properties like `transform` and `opacity`.

\
**Why it matters:** Ensures smoother animations and reduces performance issues.

***

### svg transform reliability

Wrap SVG elements in `<g>` and use `transform-box: fill-box;`.

\
**Why it matters:** Prevents inconsistent scaling and transform-origin issues across
browsers, especially Safari.

***

## layout

### optical alignment

Adjust ±1px when visual alignment feels better than strict geometric alignment.

\
**Why it matters:** Small tweaks improve perceived precision and aesthetic harmony.

***

### respect safe areas

Use CSS environment variables (`env(safe-area-inset-*)`) for devices with notches.

\
**Why it matters:** Prevents content from being hidden or clipped on modern devices.

***

### remove accidental scrollbars

Fix unintentional overflow issues.

\
**Why it matters:** Avoids visual clutter and unexpected scroll behavior, keeping
the interface clean.

***

### let css do the sizing

Use `display: flex` or `grid` rather than measuring with JavaScript.

\
**Why it matters:** Reduces layout thrashing and ensures responsive layouts without
heavy scripting.

***

## content

### curly quotes

Use “smart” quotes instead of straight quotes.

\
**Why it matters:** Improves typographic polish and readability.

***

### avoid widows/orphans

Use CSS or manual edits to prevent single words on new lines.

\
**Why it matters:** Keeps text visually pleasing and readable.

***

### tabular numbers

Use `font-variant-numeric: tabular-nums;` for aligned numeric data.

\
**Why it matters:** Helps users compare numbers easily in tables or financial data.

***

### color + text for status

Don’t rely solely on color—add text labels.

\
**Why it matters:** Ensures status is accessible to colorblind users.

***

### proper ellipsis character

Use `…` instead of `...`.

\
**Why it matters:** Maintains typographic consistency.

***

### anchored headings

Use `scroll-margin-top` for in-page links.

\
**Why it matters:** Linked headings don’t get hidden under fixed headers.

***

### locale-aware formatting

Use the `Intl` API for dates, times, and numbers.

\
**Why it matters:** Displays content in user-friendly formats for different regions.

***

### name icon-only buttons

Always provide `aria-label`.

\
**Why it matters:** Improves accessibility for screen readers.

***

### prefer native elements

Use `<button>`, `<label>` and `<a>` before ARIA attributes.

\
**Why it matters:** Native elements provide better browser behavior and accessibility.

***

### add a skip link

Include a “Skip to content” link at the top of pages.

\
**Why it matters:** Allows keyboard users to bypass repetitive navigation links.

***

### non-breaking spaces

Use `&nbsp;` for units and shortcuts, e.g., `10&nbsp;MB`.

\
**Why it matters:** Keeps important elements together for readability.

***

## forms

### enter submits single-field forms

Pressing Enter should submit forms with a single input.

\
**Why it matters:** Improves efficiency and follows user expectations.

***

### textarea shortcuts

Allow Cmd/Ctrl + Enter for submission in `<textarea>`.

\
**Why it matters:** Prevents accidental line breaks from causing confusion.

***

### labels for every control

Every input must have a visible `<label>` or accessible label.

\
**Why it matters:** Essential for screen reader accessibility.

***

### label activates input

Clicking the label should focus the input.

\
**Why it matters:** Provides a larger, more user-friendly click target.

***

### don’t block typing

Allow users to type anything; validate after input.

\
**Why it matters:** Reduces frustration and makes error messages clear.

***

### never disable submit early

Keep submit enabled until the request is in-flight.

\
**Why it matters:** Shows users errors immediately without preventing interaction.

***

### place errors next to fields

Show validation messages next to the relevant input.

\
**Why it matters:** Users immediately know what needs correction.

***

### correct autocomplete attributes

Set meaningful `autocomplete` values.

\
**Why it matters:** Enables autofill and speeds up data entry.

***

### disable spellcheck where unnecessary

Turn off spellcheck for emails, usernames, codes.

\
**Why it matters:** Reduces false warnings and visual noise.

***

### correct types + inputmode

Use proper `type` and `inputmode` for better keyboards and validation.

\
**Why it matters:** Improves mobile input efficiency and prevents errors.

***

### placeholders end with ellipsis

Use placeholders to indicate example values like `+1 (123) 456-7890…`.

\
**Why it matters:** Signals to users that input is expected.

***

### avoid triggering password managers accidentally

Give non-auth fields unique names.

\
**Why it matters:** Prevents autofill confusion for password managers.

***

### trim trailing whitespace

Automatically trim input values.

\
**Why it matters:** Avoids validation errors due to accidental spaces.

***

### windows select fix

Set explicit background and text color for native `<select>` on Windows.

\
**Why it matters:** Prevents contrast issues in dark mode.

***

## performance

### preconnect to cdns

Use `<link rel="preconnect">` for key assets.

\
**Why it matters:** Reduces DNS/TLS latency and improves perceived load time.

***

### preload fonts

Preload essential fonts for first-view text.

\
**Why it matters:** Avoids flashes of unstyled text and layout shifts.

***

### subset fonts

Ship only the necessary Unicode characters.

\
**Why it matters:** Reduces bundle size and speeds up load time.

***

### preload above-the-fold images

Prioritize images visible on the first screen.

\
**Why it matters:** Improves perceived page speed.

***

### prevent image cls

Always set width and height on images.

\
**Why it matters:** Avoids layout shifts that confuse users.

***

### quick performance testing

Throttle CPU/network to simulate real-world conditions.

\
**Why it matters:** Ensures your site performs well for all users.

***

## design

### crisp borders

Use semi-transparent borders and shadows for sharper edges.

\
**Why it matters:** Adds subtle depth and visual clarity.

***

### nested radii

Ensure child elements have equal or smaller radii than parents.

\
**Why it matters:** Maintains harmonious curves and clean UI.

***

### hue-consistent shadows

Match shadow colors to background hue.

\
**Why it matters:** Creates a cohesive, polished appearance.

***

### higher contrast on interaction

Hover, focus, and active states should increase contrast.

\
**Why it matters:** Gives clear visual feedback to users.

***

### match browser theme color

Set `<meta name="theme-color">` to align browser UI.

\
**Why it matters:** Provides a seamless experience on mobile devices.

***

### color-scheme for dark/light

Use `color-scheme` on `<html>` for scrollbars and built-in UI.

\
**Why it matters:** Ensures consistency across themes and system UIs.

***

## License

Licensed under the [MIT license](https://github.com/harshkanjiya/harshkanjiya.com/blob/main/LICENSE).

The source code is available on [GitHub](https://github.com/harshkanjiya/harshkanjiya.com). You're free to use my code! Just remove personal information before publishing. It's great to see these UX improvements being helpful.


Last updated on November 15, 2025

---
title: "Welcome to harshkanjiya.com"
description: "A dev portfolio with clean & minimal ui to showcase my work."
last_updated: "November 4, 2025"
source: "https://harshkanjiya.com/blog/my-first-blog"
---

# Welcome to harshkanjiya.com

A dev portfolio with clean & minimal ui to showcase my work.

## Special Thanks

Special thanks to [@chanhdai](https://chanhdai.com) for the amazing UI inspiration that helped shape this website's design.

## Tech Stack

* Next.js 16
* Tailwind CSS v4
* shadcn/ui

## Featured

* Clean & modern design
* Light & Dark theme support
* SEO optimization: [JSON-LD schema](https://json-ld.org), sitemap, robots
* Spam-protected email
* Installable PWA

## Blog

* MDX & Markdown support
* Syntax Highlighting for better readability
* Dynamic OG Images for rich previews

## License

Licensed under the [MIT license](https://github.com/harshkanjiya/harshkanjiya.com/blob/main/LICENSE).

The source code is available on [GitHub](https://github.com/harshkanjiya/harshkanjiya.com). You're free to use my code! Just make sure to remove all my personal information before publishing your website. It's awesome to see my code being useful to someone!


Last updated on November 4, 2025