typescript 52 lines · 8 steps

A CSS-driven progress ring in Angular

A standalone Angular component draws an SVG progress ring by pushing computed geometry into CSS custom properties via host bindings.

Explained by highlit
1import { Component, HostBinding, Input } from '@angular/core';
2 
3type ProgressVariant = 'success' | 'warning' | 'danger';
4 
5@Component({
6 selector: 'app-progress-ring',
7 standalone: true,
8 template: `
9 <svg viewBox="0 0 120 120">
10 <circle class="track" cx="60" cy="60" r="54" />
11 <circle class="indicator" cx="60" cy="60" r="54" />
12 </svg>
13 <span class="label">{{ value }}%</span>
14 `,
15 styleUrl: './progress-ring.component.css',
16})
17export class ProgressRingComponent {
18 private readonly circumference = 2 * Math.PI * 54;
19 
20 @Input() variant: ProgressVariant = 'success';
21 
22 @Input({ required: true })
23 set value(percent: number) {
24 this._value = Math.min(100, Math.max(0, percent));
25 }
26 get value(): number {
27 return this._value;
28 }
29 private _value = 0;
30 
31 @HostBinding('style.--ring-circumference')
32 get circumferenceVar(): string {
33 return `${this.circumference}px`;
34 }
35 
36 @HostBinding('style.--ring-offset')
37 get offsetVar(): string {
38 return `${this.circumference * (1 - this.value / 100)}px`;
39 }
40 
41 @HostBinding('style.--ring-color')
42 get colorVar(): string {
43 return `var(--tone-${this.variant})`;
44 }
45 
46 @HostBinding('attr.role') readonly role = 'progressbar';
47 
48 @HostBinding('attr.aria-valuenow')
49 get ariaValue(): number {
50 return this.value;
51 }
52}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Host bindings that return computed CSS custom properties let the browser handle rendering while the component owns the math.
  2. 2A setter on an @Input is the natural place to validate and clamp incoming values before they reach the view.
  3. 3Mirroring visual state into ARIA attributes keeps a custom control accessible to assistive technology.

Related explainers

typescript
import { Injectable, signal, computed, effect, inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
 
export type Theme = 'light' | 'dark';

A signal-based theme service in Angular

signals reactivity dependency-injection
Intermediate 7 steps
typescript
import { Injectable } from '@angular/core';
import { HttpClient, HttpEventType, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, distinctUntilChanged, scan } from 'rxjs/operators';

Tracking upload progress in Angular

rxjs http-events state-reduction
Intermediate 8 steps
typescript
import { Controller, Param, Sse, MessageEvent } from '@nestjs/common';
import { Observable, interval, merge } from 'rxjs';
import { filter, map, takeWhile } from 'rxjs/operators';
import { JobService } from './job.service';

Streaming job progress with SSE in NestJS

server-sent-events reactive-streams rxjs
Intermediate 8 steps
typescript
import {
  Controller,
  All,
  Req,

Catch-all routes and error shaping in NestJS

exception-handling routing middleware
Intermediate 6 steps
typescript
import {
  ArgumentsHost,
  Catch,
  ConflictException,

Turning TypeORM lock errors into 409s in NestJS

exception-handling optimistic-locking http-status
Intermediate 6 steps
typescript
import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { NgFor } from '@angular/common';
 

Building an active-route navbar in Angular

routing standalone-components accessibility
Intermediate 6 steps

Share this explainer

Here's the card — post it anywhere.

A CSS-driven progress ring in Angular — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code