ng2-excel-viewer
v0.1.2
Published
Read-only Excel (.xlsx) viewer for Angular — multiple sheets, merged cells, column/row sizing, and number/date/currency formatting. Built on SheetJS + Angular CDK virtual scroll.
Maintainers
Readme
ng2-excel-viewer
A lightweight, read-only Excel (.xlsx) viewer component for Angular. It loads a
workbook and renders it as a clean, performant grid with sheet tabs, merged cells,
column/row sizing, resizable columns, and number/date/currency formatting.
It is built on top of SheetJS for parsing and Angular CDK for virtual scrolling — the package itself is the Angular UI layer, MIT licensed.
Scope: read-only viewing. No editing, no formula recalculation. See the roadmap.
Features
- Open
.xlsx,.xls, and.csv(any format SheetJS can read) from aFile/Blob, a URL, raw bytes, or a pre-parsed workbook - Multiple sheets with a sheet-tab strip
- Merged cells, column widths, row heights
- Number / date / currency formatting
- Virtual scrolling for large sheets (tens of thousands of rows)
- Drag-to-resize columns
- Themeable via CSS variables
- Standalone component — works in standalone and NgModule apps
Requirements
- Angular 20 (peer dependency
^20.0.0) - @angular/cdk 20
- SheetJS (
xlsx) — see install note below
Installation
npm install ng2-excel-viewer @angular/cdkThat's it — SheetJS is bundled in as a dependency (pulled from the SheetJS CDN, since
the public npm xlsx package is abandoned at 0.18.5), so it installs automatically. No
separate SheetJS install, no NgModule registration, and no provide*() setup required.
Restricted/offline networks: because SheetJS is fetched from
cdn.sheetjs.comat install time, environments that only allow your private registry may block it. In that case, mirror the tarball in your registry, or use the peer-dependency setup instead — removexlsxfrom this package's resolution and install it yourself:npm install https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz. Providing your ownxlsx(including a SheetJS Pro build for full styling) also takes precedence.
Quick start (standalone app)
Import the component directly into any standalone component's imports:
import { Component, signal } from '@angular/core';
import { ExcelViewerComponent, ViewWorkbook } from 'ng2-excel-viewer';
@Component({
selector: 'app-root',
imports: [ExcelViewerComponent],
template: `
<input type="file" accept=".xlsx" (change)="pick($event)" />
<ng2-excel-viewer
[file]="file()"
height="600px"
(loaded)="onLoaded($event)"
(error)="onError($event)"
/>
`,
})
export class App {
readonly file = signal<File | undefined>(undefined);
pick(event: Event) {
this.file.set((event.target as HTMLInputElement).files?.[0]);
}
onLoaded(wb: ViewWorkbook) {
console.log('Sheets:', wb.sheetNames);
}
onError(err: Error) {
console.error('Failed to load workbook:', err);
}
}Give the viewer a bounded height. Virtual scrolling needs one. Either pass the
heightinput (default'480px'), or setheight="100%"and give the parent an explicit height.
Using it in an NgModule-based app
ExcelViewerComponent is a standalone component, so add it to the imports array of
the NgModule (or component) where you use it — not declarations:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ExcelViewerComponent } from 'ng2-excel-viewer';
import { ReportComponent } from './report.component';
@NgModule({
declarations: [ReportComponent],
imports: [CommonModule, ExcelViewerComponent], // <-- import the standalone component
})
export class ReportModule {}Then use <ng2-excel-viewer> in ReportComponent's template as shown above.
Recipes
Load from a URL — local asset, API endpoint, or a public/remote URL. The component fetches it and parses the response:
<!-- local asset -->
<ng2-excel-viewer src="assets/report.xlsx" height="70vh" />
<!-- public/remote URL -->
<ng2-excel-viewer
src="https://example.com/files/sales.xlsx"
height="70vh"
/>// or bind it from the component
readonly url = signal('https://example.com/files/sales.xlsx');<ng2-excel-viewer [src]="url()" />Cross-origin URLs are fetched with the browser
fetchAPI, so the file's server must send permissive CORS headers (Access-Control-Allow-Origin). If it doesn't, proxy the file through your own backend, or download it yourself and pass aBlob(below). Authenticated downloads are the same story — fetch the bytes with your auth and pass aBlob/ArrayBuffer.
Load a Blob (e.g. from fetch, an HttpClient blob response, or any API). A Blob
goes to the file input (it accepts File | Blob):
// with fetch
const blob = await fetch('https://example.com/files/sales.xlsx').then((r) => r.blob());
this.file.set(blob);
// with HttpClient
this.http.get('/api/export', { responseType: 'blob' })
.subscribe((blob) => this.file.set(blob));<ng2-excel-viewer [file]="file()" />Drag & drop:
onDrop(event: DragEvent) {
event.preventDefault();
this.file.set(event.dataTransfer?.files?.[0]);
}<div (drop)="onDrop($event)" (dragover)="$event.preventDefault()">
<ng2-excel-viewer [file]="file()" />
</div>Raw bytes (e.g. from an HttpClient arrayBuffer response):
this.http.get('/api/export', { responseType: 'arraybuffer' })
.subscribe(buf => this.data.set(buf));<ng2-excel-viewer [data]="data()" />Open a specific sheet and react to switches:
<ng2-excel-viewer
[src]="url"
[(activeSheet)]="active"
(sheetChange)="onSheetChange($event)"
/>Disable column resizing / hide sheet tabs:
<ng2-excel-viewer
[file]="file()"
[options]="{ resizableColumns: false, showSheetTabs: false }"
/>Configuration in code
Everything is configured through component inputs — there is no global config or module setup. Here is a complete component that wires up the common options:
import { Component, signal } from '@angular/core';
import {
ExcelViewerComponent,
ExcelViewerOptions,
ViewWorkbook,
ViewCell,
} from 'ng2-excel-viewer';
@Component({
selector: 'app-report',
imports: [ExcelViewerComponent],
template: `
<ng2-excel-viewer
[src]="url()"
[width]="'100%'"
[height]="'70vh'"
[(activeSheet)]="activeSheet"
[options]="options"
(loaded)="onLoaded($event)"
(error)="onError($event)"
(sheetChange)="onSheetChange($event)"
(cellClick)="onCellClick($event)"
/>
`,
})
export class ReportComponent {
readonly url = signal('assets/report.xlsx');
// Two-way bound: read or set the active sheet by index OR name.
activeSheet: number | string = 0;
// Only override what you need; the rest fall back to defaults.
readonly options: Partial<ExcelViewerOptions> = {
showSheetTabs: true,
virtualScroll: true,
resizableColumns: true,
defaultColWidthPx: 80,
defaultRowHeightPx: 24,
maxColumns: 100,
};
openSheetByName() {
this.activeSheet = 'Summary'; // jump to a sheet programmatically
}
onLoaded(wb: ViewWorkbook) {
console.log(`Loaded ${wb.sheets.length} sheets:`, wb.sheetNames);
}
onError(err: Error) {
console.error(err.message);
}
onSheetChange(e: { index: number; name: string }) {
console.log('Active sheet:', e.name);
}
onCellClick(cell: ViewCell) {
console.log(`Clicked ${cell.address} = ${cell.formatted}`, cell.value);
}
}Parsing without the UI. If you only need the data (e.g. to feed your own grid or
export), inject ExcelParserService directly:
import { inject } from '@angular/core';
import { ExcelParserService } from 'ng2-excel-viewer';
const parser = inject(ExcelParserService);
const workbook = await parser.parseFile(file); // File | Blob
// const workbook = await parser.parseUrl('a.xlsx'); // from a URL
// const workbook = parser.parseArrayBuffer(bytes); // ArrayBuffer | Uint8Array
for (const sheet of workbook.sheets) {
console.log(sheet.name, sheet.rowCount, 'x', sheet.colCount);
const a1 = sheet.cells[0][0]; // first cell
console.log(a1.value, a1.formatted); // raw value + display text
}You can also pass a pre-parsed ViewWorkbook straight to the viewer via [workbook].
API
Inputs
Provide one source (resolution priority: workbook > data > file > src):
| Input | Type | Default | Description |
| ------------- | ----------------------------- | ---------- | --------------------------------------------- |
| src | string | — | URL to fetch and render. |
| file | File \| Blob | — | From a file input or drag-drop. |
| data | ArrayBuffer \| Uint8Array | — | Raw workbook bytes. |
| workbook | ViewWorkbook | — | A pre-parsed workbook (skips parsing). |
| activeSheet | number \| string | 0 | Active sheet by index or name; two-way bindable. |
| width | number \| string | '100%' | Viewer width — number = px, string verbatim ('40rem'). |
| height | number \| string | '480px' | Viewer height — number = px, string verbatim ('70vh'). |
| options | Partial<ExcelViewerOptions> | see below | Behavior options (merged over defaults). |
Outputs
| Output | Payload | Fires when |
| ------------- | ---------------------------------- | ---------------------------------- |
| loaded | ViewWorkbook | A workbook finishes parsing. |
| error | Error | Parsing/fetching fails. |
| sheetChange | { index: number; name: string } | The active sheet changes. |
| cellClick | ViewCell | A cell is clicked. |
Options (ExcelViewerOptions)
| Option | Type | Default | Description |
| ------------------- | --------- | ------- | ------------------------------------------------------ |
| showSheetTabs | boolean | true | Show the sheet-tab strip when there's more than one sheet. |
| virtualScroll | boolean | true | Use virtualization for large sheets (> 200 rows). |
| resizableColumns | boolean | true | Allow dragging column borders to resize. |
| defaultColWidthPx | number | 64 | Fallback column width when none is specified. |
| defaultRowHeightPx| number | 22 | Fallback row height (also the virtual-scroll item size). |
| maxColumns | number | 200 | Cap on rendered columns (extra columns are dropped with a notice). |
Theming
The viewer is styled with CSS custom properties — override them on a parent element (or
:root) to match your app:
ng2-excel-viewer {
--ngx-excel-font: 'Inter', system-ui, sans-serif;
--ngx-excel-accent: #2563eb; /* active tab / resize guide */
--ngx-excel-border: #d0d0d0; /* outer + header borders */
--ngx-excel-gridline: #e4e4e4; /* cell gridlines */
--ngx-excel-header-bg: #f3f3f3; /* column/row headers */
--ngx-excel-cell-bg: #ffffff; /* cell background */
--ngx-excel-zebra-bg: #fafafa; /* alternating rows */
--ngx-excel-tabbar-bg: #f3f3f3;
--ngx-excel-tab-hover-bg: #e8e8e8;
--ngx-excel-tab-active-bg: #ffffff;
}Important: styling fidelity
SheetJS Community Edition does not decode most cell styling — author fonts, fills / background colors, bold/italic, borders, and alignment overrides are a SheetJS Pro ("Style") feature and are not present in the parsed data.
What this viewer does render faithfully from the community edition:
- Cell values and types
- Number / date / currency formatting (via the cell's format code)
- Merged cells
- Column widths and row heights
- Multiple sheets and sheet names
- Our own clean default grid styling (gridlines, header, zebra striping, numbers right-aligned)
The style plumbing is fully wired, so if you drop in a SheetJS Pro build (identical
xlsx import surface), author styling lights up with no code change.
Notes & limitations (v1)
- Server-Side Rendering (SSR): parsing relies on browser APIs (
File,fetch,ArrayBuffer). Render the viewer on the client (e.g. guard withafterNextRenderor anisPlatformBrowsercheck) — don't feed it a source during server rendering. - Wide sheets: columns are not virtualized; they scroll horizontally. The
maxColumnscap (default 200) protects against pathologically wide sheets. - Large sheets (> 200 rows) use vertical virtual scrolling with a fixed row height. A vertical merge whose anchor has scrolled out of the viewport shows blank covered cells until the anchor returns. Small sheets use a plain table with full merge fidelity.
- Resizing is view-only — it doesn't modify the file, and widths reset when a new workbook loads.
Roadmap
- True 2D virtualization for very wide sheets
- Variable row heights under virtualization
- Optional light editing +
.xlsxexport
License
MIT
