यन्त्रम् · ANGULAR 22+

@code_with_sachin/ngx-style-qr

Styled QR codes as SSR-safe SVG. Zero runtime dependencies.

INSTALL

BASH
npm i @code_with_sachin/ngx-style-qr

Why

Most QR libraries draw to a canvas in the browser, which means an empty box during server-side rendering and a pop when hydration catches up. This one encodes and draws in pure functions, so the symbol is already in the server-rendered HTML. The QR encoder — Reed–Solomon, masking, all 40 versions — is written into the package, so there is nothing to install alongside it.

  • Zero runtime dependencies; Angular is the only peer.
  • SVG output — crisp at any size, themeable, printable.
  • Every shape combination is rasterised and read back by a real decoder in CI, at three resolutions, so styling never quietly breaks scannability.

Requirements

TESTED AGAINST

NAMETYPEDEFAULTNOTES
@angular/core^22.0.0 22.0.5 Standalone APIs, signal inputs and afterNextRender are all required.
typescript~6.0.0 6.0.3 Whatever your Angular version supports.
node>=20 24.15.0 Build and SSR only.

Peer ranges are wider than this — the table lists the exact versions the demos on this page are running, so you have a known-good combination to fall back on.

Providers

Nothing to register. This package exposes no provider function and reads no injection token — import the component where you use it and you are done. Your app.config.ts needs no change.

Using NgModules

Everything here is standalone, but standalone components and directives are importable from an @NgModule — put them in the module's imports, not declarations. No importProvidersFrom is needed: @NgModule.providers is typed Array<Provider | EnvironmentProviders>, so the provide*() functions drop straight in.

TS · app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { StyleQrComponent } from '@code_with_sachin/ngx-style-qr';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  // Standalone components and directives go in `imports`.
  imports: [
    BrowserModule,
    StyleQrComponent,
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Quick start

Standalone component — import it where you use it. No module, no provider.

TS · share.component.ts
import { Component } from '@angular/core';
import { StyleQrComponent } from '@code_with_sachin/ngx-style-qr';

@Component({
  selector: 'app-share',
  imports: [StyleQrComponent],
  template: `<ngx-style-qr data="https://sachinsingh.me" />`,
})
export class Share {}

Playground

Every input below maps one-to-one to a template binding. Scan the result with your phone as you change things — that is the only test that counts.

https://sachinsingh.me
MODULE SHAPE
EYE SHAPE
ERROR CORRECTION
MODULE PAINT
BACKGROUND
EYE RING
EYE CENTRE

Read as a data URI, so PNG export keeps working.

Inputs

<NGX-STYLE-QR>

NAMETYPEDEFAULTNOTES
datastringRequired. Numeric, alphanumeric and UTF-8 byte modes are picked automatically.
sizenumber 256 Rendered width/height in CSS pixels. The SVG scales losslessly, so this is presentation only.
ecLevel'L' | 'M' | 'Q' | 'H' 'M' Error correction: ~7% / 15% / 25% / 30% recoverable. Use H whenever a logo is set.
marginnumber 4 Quiet zone in modules. The spec requires 4; lower hurts scan rates.
moduleShape'square' | 'dots' | 'rounded' | 'smooth' 'square' smooth is neighbour-aware — runs of modules merge into continuous blobs.
eyeShape'square' | 'rounded' | 'circle' | 'leaf' 'square' Applied to all three finder patterns.
colorstring | QrGradient '#000000' Body modules. Any CSS colour, or a linear/radial gradient.
bgColorstring | QrGradient | 'transparent' 'transparent' Full-canvas background, quiet zone included.
eyeColorstring | QrGradient | null null Finder rings. Falls back to color.
eyeInnerColorstring | QrGradient | null null Finder centres. Falls back to eyeColor, then color.
logoQrLogo | null null Centre image with automatic module erasure. Use a data URI.
labelstring '' Accessible name emitted as <title>. Falls back to data.

Styling

Colours accept a CSS colour string or a gradient object. Gradient ids are derived from the gradient's own content rather than a counter, so the server and client renders agree and url(#…) survives hydration.

HTML
<ngx-style-qr
  data="https://sachinsingh.me"
  ecLevel="H"
  moduleShape="smooth"
  eyeShape="rounded"
  [size]="320"
  [color]="{
    type: 'linear',
    rotate: 45,
    stops: [{ offset: 0, color: '#c21b2e' }, { offset: 1, color: '#f4771f' }]
  }"
  bgColor="#0a0a0a"
  eyeColor="#c9a24b"
  label="Scan for my portfolio"
/>

Centre logo

The modules under the logo are erased, not covered — that comes straight out of the error-correction budget, so pair a logo with ecLevel="H". The component warns in dev mode if you don't.

TS
// A data URI, not a URL — a cross-origin image taints the export
// canvas and makes download(..., 'png') reject with a SecurityError.
protected readonly logo = {
  src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0i…',
  size: 0.22,      // fraction of the symbol, clamped to 0.05–0.35
  padding: 1,      // extra modules cleared around it
  bgColor: '#fff', // optional plate behind the image
  bgRadius: 2,
};

Download

TS
@Component({
  template: `
    <ngx-style-qr #code data="https://sachinsingh.me" [size]="512" />
    <button (click)="code.download('portfolio', 'png')">PNG</button>
    <button (click)="code.download('portfolio', 'svg')">SVG</button>
  `,
})
export class Share {}

// Or from TypeScript:
readonly code = viewChild.required(StyleQrComponent);
save() { void this.code().download('portfolio', 'png'); }

Without the component

The encoder and renderer are exported on their own, for painting a matrix into a canvas, a terminal, or a print pipeline.

TS
import { encodeQr, buildSvgModel } from '@code_with_sachin/ngx-style-qr';

const qr = encodeQr('https://sachinsingh.me', 'H');
qr.version;         // 3
qr.size;            // 29  (modules per side)
qr.modules[0][0];   // true — top-left finder pattern

// …then paint it however you like: canvas, terminal, PDF, print pipeline.
for (const row of qr.modules) {
  console.log(row.map((dark) => (dark ? '██' : '  ')).join(''));
}

METHODS & FUNCTIONS

NAMETYPEDEFAULTNOTES
download(filename?, format?)(string, 'svg' | 'png') => Promise<void> ('qr-code', 'png') Saves the rendered code. No-op during SSR. PNG rasterises at 2× size.
encodeQr(data, ecLevel?)(string, QrEcLevel) => QrCode ('M') Standalone encoder. Throws if the payload exceeds version 40 at that level.
buildSvgModel(qr, options)(QrCode, StyleQrOptions) => QrSvgModelPure renderer — path data, gradient defs and logo box. No DOM.

Notes

Capacity

encodeQr throws — with the byte count and the limit — if the payload cannot fit version 40: roughly 2953 bytes at level L, 1273 at level H. Kanji mode is not implemented.

Accessibility

The SVG always carries role="img" and a <title>. Set label when the payload is a URL a screen-reader user would not want read out character by character.

PNG export

Rasterised through an offscreen canvas at 2× size. A cross-origin logo taints that canvas, so keep logo.src a data URI. A transparent bgColor stays transparent in the PNG.

Dots geometry

The dots shape draws circles slightly larger than one module. Tangent circles cover only π/4 of a cell, which drops the local dark ratio far enough that adaptive binarisers start misreading whole blocks.

Built for sachinsingh.me — this package ships from that portfolio's own workspace.

MIT · Sachin Singh