Full Technical Comparison
Looking for the short version? See Compare with Material and PrimeNG for the quick, visual summary — this page is the backing data: every number, the command that produced it, and the raw byte counts, so you can check the arithmetic yourself.
Angular Material and PrimeNG are both excellent, battle-tested libraries — this isn't about tearing them down. It's a straight, numbers-first comparison, built to answer the question every team asks before adopting a UI library: what does it actually cost to bring in, and how much fighting does it take to make it look like your product?
Guild of Gleks UI ships 32 documented components against PrimeNG's 90+ and Material's ~35. That's a real, honest tradeoff — see What this library doesn't try to be below. What follows is everything else: bundle weight, CSS weight, dependency depth, legacy surface and theming model, all measured against the two libraries' own published npm packages.
Measured on
| Date | 2026-09-13, all three libraries |
@guildofgleks/ui |
21.14.0 |
@angular/material / @angular/cdk |
22.1.6 |
primeng |
22.1.1 |
| Bundler | esbuild 0.28.2, --bundle --minify --format=esm |
| Compression | node:zlib gzipSync, level 9 |
| Toolchain | Node 24.15.0, npm 11.12.1 |
Sizes are reported in bytes and in KB, where 1 KB = 1024 bytes and 1 MB = 1024 KB.
The one exception is dist.unpackedSize, quoted straight from the registry in bytes.
Full re-measurement, all three libraries, every table — the previous pass was 2026-09-02 against 21.7.2, and seven minors of this library have shipped since. Material and PrimeNG each moved one patch and barely moved at all. Three things did, and they are worth stating before the tables rather than leaving as unexplained deltas:
@guildofgleks/ui's whole-library bundle grew from 112.8 KB to 123.1 KB gzipped across seven minors of component work — virtualization in four collection components andgog-alertamong it; this page measures the total and does not attribute it. It is still smaller than four Material components (153.6 KB), but the gap narrowed from 1.36× to 1.25×, and the page says so rather than keeping the old ratio.- Its stylesheet nearly doubled:
index.css, which is whatREADME.mdtells you to include, went from 29.8 KB to 51.4 KB gzipped. Most of that is prose —theme.css's comments are the library's design record and are about three quarters of its gzipped size. That trade is an open decision in the repository's backlog, and this page measures what ships today. - The whole-library recipe changed shape. Since 21.14.0 the table, datepicker and dialog are
exported only from
@guildofgleks/ui/table,/datepickerand/dialog, so "everything" is fourexport *lines, not one. Bundling the root alone would silently drop three components.
Every figure below moves when any of these libraries publishes. Re-run the bench in the next section against whatever is current — that is the whole point of publishing the commands rather than the conclusions.
Reproduce every number on this page
Roughly two minutes end to end, most of it npm. Nothing here needs an Angular workspace: each library is installed on its own, bundled from its real published entry points, and measured.
1. Set up three isolated folders. Each library has to be measured against exactly the dependencies it brings: installed together, npm dedupes what they share and the bundles stop reflecting what adding one library to your app would actually cost.
(Until 21.5.0 there was a harder reason — @guildofgleks/ui peered on Angular 21 alone while
current Material and PrimeNG peer on 22, so npm's peer resolution refused the shared install
outright. Its range is ^21.2.0 || ^22.0.0 now, so that conflict is gone; the folders stay
separate for the measurement, not for the installer.)
mkdir bench && cd bench
mkdir gleks material primeng
(cd gleks && npm init -y && npm install @guildofgleks/[email protected] esbuild)
(cd material && npm init -y && npm install @angular/[email protected] @angular/[email protected] esbuild)
(cd primeng && npm init -y && npm install [email protected] esbuild)
Swap the pinned versions for @latest to measure today's releases instead; check the
current ones first with npm view @guildofgleks/ui version, npm view @angular/material version, npm view primeng version.
2. Bundle. Same esbuild invocation for all three — only the entry file changes. The
Angular framework and rxjs/tslib are marked external, because every Angular app pays
for those once regardless of which UI library it picks; everything else a library imports
is counted, including PrimeNG's own runtime packages and Material's @angular/cdk:
# from gleks/ — the root plus the three split entry points (the internal /shared comes with them)
printf "export * from '@guildofgleks/ui';\nexport * from '@guildofgleks/ui/table';\nexport * from '@guildofgleks/ui/datepicker';\nexport * from '@guildofgleks/ui/dialog';\n" > entry-all.mjs
npx esbuild entry-all.mjs --bundle --minify --format=esm \
--external:@angular/core --external:@angular/common --external:@angular/forms \
--external:@angular/platform-browser --external:rxjs --external:tslib \
--outfile=out-all.min.js
# from material/ and primeng/ — no combined entry point, so one file per component
for c in button select dialog table; do
echo "export * from '@angular/material/$c';" > entry-$c.mjs # material/
echo "export * from 'primeng/$c';" > entry-$c.mjs # primeng/
done
# the invocation, run in each folder for each entry-*.mjs
npx esbuild entry-button.mjs --bundle --minify --format=esm \
--external:@angular/core --external:@angular/common --external:@angular/forms \
--external:@angular/platform-browser --external:rxjs --external:tslib \
--outfile=out-button.min.js
This is the recipe behind the "Entire library, gzipped" row below — it was missing from
this page until 2026-08-28 (the folder created entry-all.mjs but never showed the command
that bundled it), which cost more to recover than the measurement itself did. The externals
list matters: without it, esbuild inlines Angular itself and every number in this section
balloons to megabytes.
3. Measure. This one-liner prints both numbers for every bundle in the folder and is identical across platforms:
node -e "const fs=require('fs'),z=require('zlib');let m=0,g=0;
for(const f of fs.readdirSync('.').filter(f=>f.endsWith('.min.js'))){
const b=fs.readFileSync(f),gz=z.gzipSync(b,{level:9}).length;m+=b.length;g+=gz;
console.log(f,b.length,gz);}
console.log('SUM',m,g);"
Do not substitute the gzip CLI for this. gzip -9 and zlib.gzipSync(…, {level: 9})
compress the same bytes to measurably different output sizes (checked 2026-08-28: 116 KB via
the CLI vs. 116 355 B via zlib on the same input) — same algorithm, different encoder
internals. Every number on this page is zlib-gzipped; mixing in a CLI-gzipped figure makes
two cells in the same table not comparable.
4. Everything else — package size, dependency tree, deprecated API, NgModules, component counts — is one command each, listed in its own section below:
| Claim | Section | Command |
|---|---|---|
| Published package size | Package size | npm view <pkg> dist.unpackedSize |
| What gets installed | Dependency depth | npm ls --all --parseable |
| Deprecated API | Legacy surface | grep -rc '@deprecated' <pkg>/types |
| NgModule classes | Legacy surface | grep -rh 'declare class.*Module\b' <pkg>/types |
| Component counts | How many components | grep -o 'ɵɵComponentDeclaration<.*?, "…"' |
| CSS you must ship | The CSS nobody counts | wc -c <pkg>/**/*.css |
The short version
| Guild of Gleks UI | Angular Material | PrimeNG | |
|---|---|---|---|
| Documented components | 32 | ~35 | 90+ |
| Packages installed beyond Angular | 0 | 3 | 11 |
Runtime dependencies in package.json |
1 (tslib) |
1 (tslib) + required @angular/cdk peer |
6 + tslib |
| npm package, unpacked | 3 865 623 B (3.69 MB) | 7 681 586 B (7.33 MB) + CDK 3 572 735 B (3.41 MB) | 14 089 483 B (13.44 MB) |
| Button + Select + Dialog + Table, gzipped | (not per component — see below) | 157 237 B (153.6 KB) | 340 718 B (332.7 KB) |
| Entire library, gzipped | 126 011 B (123.1 KB) | (no combined entry point) | (no combined entry point) |
| Required stylesheet, gzipped | 52 589 B (51.4 KB) | 1 296 B (1.3 KB, M3 prebuilt theme) | 0 — injected at runtime from JS |
@deprecated symbols in the package |
0 | 36 | 34 |
| …that name a removal version | n/a — 0 deprecated (28 symbols and 3 tokens removed in 21.14.0) | 42 @breaking-change tags, 40 of them already overdue |
0 of 34 |
NgModule classes shipped |
0 | 43 | 113 |
| Theming | Plain CSS custom properties, no build step | Sass mixins / M3 system tokens | JS preset system (@primeuix/styled) |
The row worth re-reading is the pair in the middle: the whole Guild of Gleks UI library, gzipped, is 1.25× smaller than four Material components and 2.70× smaller than the same four from PrimeNG — both ratios down from 1.36× and 2.95× at 21.7.2, because this library grew and the other two did not.
Bundle weight, measured
The comparable slice: Button + Select + Dialog + Table
Material and PrimeNG only make sense measured per component — they are designed to be cherry-picked. These four are the closest match across all three catalogues, bundled individually and then summed:
| Component | Material min | Material gz | PrimeNG min | PrimeNG gz |
|---|---|---|---|---|
| Button | 189 789 B (185.3 KB) | 23 765 B (23.2 KB) | 165 661 B (161.8 KB) | 36 293 B (35.4 KB) |
| Select | 356 294 B (347.9 KB) | 69 309 B (67.7 KB) | 366 696 B (358.1 KB) | 72 675 B (71.0 KB) |
| Dialog | 189 945 B (185.5 KB) | 41 903 B (40.9 KB) | 268 283 B (262.0 KB) | 54 169 B (52.9 KB) |
| Table | 118 652 B (115.9 KB) | 22 260 B (21.7 KB) | 1 105 335 B (1079.4 KB) | 177 581 B (173.4 KB) |
| Sum | 854 680 B (834.6 KB) | 157 237 B (153.6 KB) | 1 905 975 B (1861.3 KB) | 340 718 B (332.7 KB) |
PrimeNG's table is the outlier of the whole comparison: on its own it gzips to 173.4 KB —
1.41× this library's entire catalogue — because primeng/table is a full data grid with
filtering, grouping, frozen columns, resize and reorder built in. That is a feature
difference, not waste; see what this library doesn't try to be.
Caveat, in the honest direction: summing four independently-bundled files overstates the real cost for an app that uses all four together, because a production bundler dedupes the chunks they share. The true combined figure is somewhat smaller than these sums, for both libraries.
Why this library has no column here. Its table and dialog do have entry points now, but they
import the rest of the package from its root, and a bundle of the root is the whole root: the
published code is partially compiled, and what makes a component shakable is written in by the
Angular linker in your build, which esbuild alone does not run. A per-component row would
measure most of the library four times. The whole-library row below is the honest number, and
code-splitting has what an app actually pays for one
component, measured in a real CLI build.
Whole-library cost
Guild of Gleks UI can be imported whole — the root and its three split entry points — so a real "cost of everything" number exists:
| Library | Minified | Gzipped |
|---|---|---|
| @guildofgleks/ui — all 36 components, 3 services, 32 directives | 883 078 B (862.4 KB) | 126 011 B (123.1 KB) |
| @angular/material | (no combined entry point) | (no combined entry point) |
| primeng | (no combined entry point) | (no combined entry point) |
Neither of the other two can produce this number, and it is not an oversight on their
part: both resolve their root "." export to a generated stub. Check it yourself —
cat node_modules/@angular/material/fesm2022/material.mjs # 3 lines, exports one marker const
cat node_modules/primeng/fesm2022/primeng.mjs # 107 bytes, `var public_api = {}`
so "the whole library" is not a thing you can import from either, by design.
For reference on the same bench: 21.3.0 measured 92.8 KB gzipped, 21.4.1 103.8 KB, 21.6.0
107.6 KB, 21.6.1 113.6 KB, 21.7.2 112.8 KB — the first decrease this table recorded — and 21.14.0
is 123.1 KB. The 21.7.2 decrease, for the record: gog-card, gog-panel and gogRipple (21.6.1) are still in the bundle; what left
is the 154-entry GOG_DEPRECATIONS manifest, emptied to [] when 21.7.0 removed the three
abbreviated token prefixes it existed to track. 21.7.0's other headline work — the character
layer, six new theme presets — added nothing here because it is entirely theme.css, a
separate row below (and one that did grow). A JS bundle can shrink release over release
without the library doing any less; it just means the thing that shrank was metadata, not
component code. The growth since is the other way round — seven minors of component work, not
attributed release by release here.
Code-splitting
Tree-shaking and code-splitting are different promises. Measured on a fresh CLI application
(production build, estimated transfer size): a page with one gog-button costs 10.2 kB over
Angular's own 50.9 kB, so an app pays for what it imports. Splitting is narrower. On 21.13.0 a route
that loaded gog-table, gog-datepicker, gog-calendar and gog-dialog through loadComponent
produced a lazy chunk of 442 bytes, and the components stayed in the initial bundle.
21.14.0 exports those three only from their own entry points — @guildofgleks/ui/table,
/datepicker and /dialog — and the same app measured again:
| Same app, heavy four behind one lazy route | Initial | Lazy chunk |
|---|---|---|
| 21.13.0 | 101.2 kB | 442 B |
| 21.14.0 | 88.2 kB | 17.0 kB |
| No heavy route at all (the floor) | 61.2 kB | — |
The other 27 kB is what those components import from the root — paginator, select, checkbox, scroll, spinner, icon, button — and it stays in the initial bundle, because the root is one module and the first page already imports it. That is true of any root component used only behind a lazy route, so this is a saving on the three heaviest components' own code, not a general answer.
Package size on the registry
What npm actually stores and unpacks, straight from the registry:
npm view @guildofgleks/[email protected] dist.unpackedSize # 3865623
npm view @angular/[email protected] dist.unpackedSize # 7681586
npm view @angular/[email protected] dist.unpackedSize # 3572735
npm view [email protected] dist.unpackedSize # 14089483
This is disk, not download weight — it includes type definitions, source maps and, for
Material, Sass sources. It matters for CI cache size and node_modules bloat, not for
your users.
The CSS nobody counts
Bundle comparisons usually stop at JavaScript, which flatters whichever library moves the most styling into JS. All three put their component CSS inside the JS bundles measured above; what differs is the token/theme layer you import separately:
| Library | File | Raw | Gzipped |
|---|---|---|---|
| @guildofgleks/ui | styles/theme.css (required — every token the components read) |
178 860 B (174.7 KB) | 40 801 B (39.8 KB) |
| @guildofgleks/ui | styles/index.css (theme + typography + utilities + button + menu + surfaces + ripple) |
225 105 B (219.8 KB) | 52 589 B (51.4 KB) |
| @angular/material | prebuilt-themes/azure-blue.css (M3) |
7 394 B (7.2 KB) | 1 296 B (1.3 KB) |
| @angular/material | prebuilt-themes/indigo-pink.css (legacy M2) |
110 763 B (108.2 KB) | 9 649 B (9.4 KB) |
| primeng | — none; @primeuix/styled generates CSS at runtime |
0 B | 0 B |
find node_modules/@guildofgleks/ui/styles -name '*.css' | xargs wc -c
find node_modules/@angular/material/prebuilt-themes -name '*.css' | xargs wc -c
find node_modules/primeng -name '*.css' | wc -l # 0
# gzipped, same zlib recipe as every other number on this page — not the `gzip` CLI
node -e "const fs=require('fs'),z=require('zlib');
console.log(z.gzipSync(fs.readFileSync('node_modules/@guildofgleks/ui/styles/theme.css'),{level:9}).length)"
# index.css is a list of @import lines; an Angular build inlines them into one stylesheet, so its
# row is the seven files concatenated in that order and gzipped together
node -e "const fs=require('fs'),z=require('zlib'),p='node_modules/@guildofgleks/ui/styles/';
const b=Buffer.concat(['theme','typography','utilities','button','menu','surfaces','ripple'].map(f=>fs.readFileSync(p+f+'.css')));
console.log(b.length,z.gzipSync(b,{level:9}).length)"
Read this row against us, not for us. Material's M3 prebuilt theme is 31× smaller
gzipped than theme.css, because it declares a palette and lets Sass bake the rest at
build time, while this library declares all 1 478 tokens as live custom properties so
they can be overridden at runtime with no build step. That is the trade: ~51 KB gzipped for
the whole of index.css, once, in exchange for retheming anything from a stylesheet or a
style attribute. Most of that is not the tokens. theme.css carries its own design record
as comments — why each value is what it is — and stripped of them it gzips to roughly a quarter of
its shipped size; whether to keep shipping that prose in the stylesheet is an open decision, not a
settled one. PrimeNG
ships no stylesheet at all — its CSS is generated in the browser from the preset, which
means it is already inside the JS numbers above and costs main-thread time instead of
bytes.
Dependency depth
The question that matters is not how many names are in dependencies, it is what ends
up in node_modules. Run in each bench folder:
npm ls --all --parseable | sed 's|.*node_modules/||' | sort
Both Material and PrimeNG pull Angular 22, so zod and @standard-schema/spec appear in
their trees — those come from @angular/forms@22 and belong to the framework, not to the
library. Discounting the framework packages every Angular app already has, what each
library adds is:
| Library | Adds | What |
|---|---|---|
| @guildofgleks/ui | 0 packages | tslib only, which Angular itself already depends on |
| @angular/material | 3 packages | @angular/cdk (a required peer, not optional), which brings parse5 → entities |
| primeng | 11 packages | @angular/cdk (+parse5, entities), @angular/router, @primeicons/angular (+@primeicons/core), @primeui/license-manager, @primeuix/styled, @primeuix/styles, @primeuix/utils, @primeuix/motion |
Two things in that last row are worth naming explicitly:
- PrimeNG now requires
@angular/cdktoo, as a peer dependency — the "Material needs the CDK, PrimeNG doesn't" distinction no longer holds. @primeui/license-managerno longer pulls a cryptographic signature stack into the install tree. An earlier pass of this page counted@noble/ed25519and@noble/hashesamong PrimeNG's 13 added packages;@primeui/[email protected]'s ownpackage.jsonlists both underdevDependencies, notdependencies, so a realnpm installnever fetches them — confirmed by reading that file directly, not by npm ls coming up empty (which could just as easily mean "not looked hard enough"). Its own compileddist/index.mjsdoesn't referenceed25519ornobleeither. Verify withnpm ls @noble/ed25519(nothing) andcat node_modules/@primeui/license-manager/package.json.
Guild of Gleks UI implements its own overlay positioning, focus trap and roving-focus primitives internally, which is why there is no utility library sitting underneath it the way the CDK sits under the other two.
Legacy surface
All three target recent Angular. The difference is what is also still in the box, counted from each package's own published type definitions:
grep -rc '@deprecated' node_modules/@angular/material/types | awk -F: '{s+=$2} END {print s}' # 36
grep -rc '@deprecated' node_modules/primeng/types | awk -F: '{s+=$2} END {print s}' # 34
grep -rc '@deprecated' node_modules/@guildofgleks/ui/types | awk -F: '{s+=$2} END {print s}' # 1
# that single hit is prose, not a tag — it is the sentence describing the deprecation manifest,
# which lives in the internal /shared entry point's types. The manifest states the real counts:
grep -rho 'currently deprecates: .*' node_modules/@guildofgleks/ui/types
# → currently deprecates: 0 symbol(s) and 0 token(s). (28 symbols and 3 tokens until 21.14.0)
grep -rh 'declare class.*Module\b' node_modules/@angular/material/types | wc -l # 43
grep -rh 'declare class.*Module\b' node_modules/primeng/types | wc -l # 113
grep -rh 'NgModule\|declare class.*Module\b' node_modules/@guildofgleks/ui/types | wc -l # 0
| Guild of Gleks UI | Angular Material | PrimeNG | |
|---|---|---|---|
@deprecated symbols |
0 | 36 | 34 |
| Deprecations naming when they disappear | n/a — 0 deprecated | 42 @breaking-change tags |
0 |
NgModule classes |
0 | 43 | 113 |
Restricted to the four-component slice the bundle section uses: Material 1 (table),
PrimeNG 5 (button), and this library 0 — it has no deprecated symbol anywhere, in that
slice or outside it.
On removal discipline. Material annotates deprecations with @breaking-change <major>,
which is a real schedule and better than nothing — but 40 of its 42 tags name a major at
or below the current one (ten of them say @breaking-change 8), so the API is still
shipping years past its own removal date. PrimeNG's 34 deprecations name no version at all.
This library had 15, all carrying @deprecated since <version> (<date>) — <replacement>. Removed in <version>. Two had overrun that date — GogSelectOption and
GogMultiselectOption were marked for removal in 21.4.0 and were still exported through
21.4.4. 21.5.0 removed all 15, the two overdue ones included, and added
npm run check:deprecations to the build: it fails on any tag whose removal version has
already been reached, so a deprecation cannot overrun its date again. The overrun is
recorded in the changelog rather than quietly re-dated.
Nothing is deprecated, as of 21.14.0 — and as of 21.7.0 before it. 21.14.0 removed the
second wave on the date it announced: 25 symbols that moved to the table, datepicker and dialog
entry points, three internal helpers, and three renamed tokens, each after one minor of overlap.
Before that: The last deprecation on the books was 154
CSS custom properties — three abbreviated prefixes (--gog-btn-*, --gog-confirm-*,
--gog-ms-*) spelled out, with every old name resolving alongside the new one for two minors
rather than one, precisely because a custom property nothing reads fails silently, which no
compiler can catch for you. 21.7.0 removed all three prefixes and their 154 old names on
schedule, so GOG_DEPRECATIONS — the same manifest that carried them at runtime with since,
sinceDate, replacement and removedIn — is now an empty array, and npm run check:deprecations still runs on every build to fail it the moment a future deprecation
overruns its own stated removal version.
grep -o 'Removed in [0-9.]*' node_modules/@guildofgleks/ui/types/guildofgleks-ui.d.ts | sort | uniq -c
# → nothing: no symbol is deprecated
grep -rho '@breaking-change [0-9]*' node_modules/@angular/material/types | sort | uniq -c
On NgModules. Material and PrimeNG both predate Angular's standalone era, so every
component still ships an NgModule wrapper (MatButtonModule, SelectModule, …) beside
the modern standalone API — extra exported surface a tree-shaker has to prove is unused.
Guild of Gleks UI started after that shift, so there was never a module system to carry
forward: every component has been standalone, signal-based and OnPush from its first
commit.
How many components, exactly
Marketing counts are hard to compare — libraries count sub-elements, directives and services differently. Two reproducible numbers instead:
# element selectors declared by components, from the published .d.ts (works on any of the
# three — point it at the package's types/ directory). Swap ɵɵComponentDeclaration for
# ɵɵDirectiveDeclaration for the directive-selector row below.
node -e "const fs=require('fs'),p='node_modules/@guildofgleks/ui/types/';
const s=fs.readdirSync(p).map(f=>fs.readFileSync(p+f,'utf8')).join('\n');
console.log([...s.matchAll(/ɵɵComponentDeclaration<.*?,\s*\"([^\"]+)\"/g)].length)" # 36 — every types file, not only the root's
# code entry points, from package.json's exports map, minus assets and test harnesses
node -e "const e=require('@angular/material/package.json').exports;
console.log(Object.keys(e).filter(k=>k!=='.'&&!k.includes('*')&&!k.endsWith('.css')
&&!k.endsWith('.json')&&!k.includes('theming')&&!k.includes('testing')).length)" # 36
| Guild of Gleks UI | Angular Material | PrimeNG | |
|---|---|---|---|
| Documented components (pages on this site) | 32 | ~35 | 90+ |
| Component selectors in the type definitions | 36 | 90 | 240 |
| Directive selectors | 32 | 99 | 69 |
| Code entry points | 4 | 36 | 282 |
The selector counts are the honest raw numbers and they flatter nobody: 36 for this
library includes several sub-elements you rarely write yourself (gog-tab,
gog-toast-container, gog-confirmation-dialog, gog-spinner-overlay), and Material's
90 likewise counts every mat-* part of a composite component. The 32 on the first row
is not a subset of the 36 — it is the site's own page count, and it counts differently: 32
element-component pages, with gogBadge, gogTooltip and gogRipple documented as three
further pages that this row does not include (the nav's own split is "32 components and 3
directives" — see components/shared/nav-data.ts). Adding them gives 35 documented pages in
total, not 32; keep the two counts (36 selectors vs. 32+3 documented pages) from different
methodologies apart rather than reconciling them into one number, because they are answering
different questions — "what does the package export" against "what does this site explain".
This library's 4 are the root, @guildofgleks/ui/table, /datepicker and /dialog — the
three components heavy enough to be worth keeping off an initial route, see
code-splitting. The command counts a fifth path the package
also exports, /shared, which is internal plumbing the other four share and not an API; the row
leaves it out. It was 1 until 21.13.0.
Material's 36 entry points line up almost exactly with its "~35 components" — one of them
(./core) is shared infrastructure rather than a component. PrimeNG's 282 do not, because
that map also exposes directives, base classes and utilities (./base, ./api,
./autofocus, …) as separate entry points; its "90+ components" is the smaller,
component-only subset of that list.
Theming
This is the part the numbers don't capture, and it is the actual reason this library exists: change one CSS custom property, or pass one input, and the component just looks right — no rebuild, no Sass recompile, no fighting specificity.
Guild of Gleks UI — every visual value is a
--gog-*CSS custom property, layered foundation → component → instance (see Theming). Retheme the whole library by overriding a handful of foundation tokens, restyle one component by overriding its own tokens, or override a single instance inline. No build step, no preprocessor, no JS theming API at any layer. The cost is the stylesheet measured in The CSS nobody counts above — every token shipped as a live custom property rather than baked at build time.The palettes are computed and gated rather than picked by eye, which is the part of a theming model that usually goes unstated.
npm run check:contrastmeasures 3 995 pairs across the eleven shipped themes — including every control boundary and every focus indicator, not only text — andnpm run check:oklchgates the three things a WCAG ratio is blind to: a state step too small to perceive, a status colour that has stopped being a colour, and two statuses a reader cannot tell apart. Both are CI steps. A third script,npm run suggest:color, does the half a check normally leaves to you: given an ink, a ground and a target ratio it returns a value that clears it holding hue and chroma, so the fix is still your theme's own neutral. All three ship in the repository and run against a fork of any preset.Angular Material — theming is built around Sass:
mat.theme(), palette definitions and per-component-overridesmixins. Material 3 introduced CSS-variable system tokens (--mat-sys-*) which help at the palette level, but granular per-component and per-instance overrides typically still route through Sass mixins or::ng-deep. The payoff is the small prebuilt theme — values are baked at build time.PrimeNG — ships a JS preset system (
@primeuix/styled,definePreset()) as separate packages from the components. It is genuinely flexible and it is also a theming engine to learn, applied at runtime rather than a token you set in plain CSS.
What this library doesn't try to be
Fair is fair: PrimeNG's 90+ components include a charting wrapper, an org chart, a rich
text editor and a dozen specialized data-entry widgets this library simply doesn't have —
and its primeng/table, the biggest single number on this page, is a full data grid with
filtering, grouping, frozen columns and row expansion. If your product needs a Gantt chart
or a tree table out of the box, PrimeNG is the right tool, full stop. Material's ecosystem
maturity and first-party CDK primitives (drag-drop, virtual scroll, portals) are equally
real building blocks this library doesn't attempt to replace.
Both halves of a large collection are covered, within limits. Since 21.13.0 gog-select,
gog-multiselect, gog-autocomplete and gog-table render only the rows in view when you set
virtualize — off by default, and the table's version needs maxHeight — while lazy mode and
gogLoadMore cover the server side. What is not here is the rest of a data grid: no column
grouping, frozen columns or row expansion.
Guild of Gleks UI covers the 32 components that show up in almost every product — buttons, forms, dates, dialogs, tables, navigation and feedback — with a small, consistent, easily restyled surface instead of a sprawling one. Pick the tool that matches what you are actually building.
Caveats worth knowing before you quote these numbers
- Externals shift the JS numbers. Marking
@angular/*,rxjsandtslibexternal is the fair choice — every Angular app pays for them once — but it means these are marginal costs of adding a library, not total download sizes. - These are the packages as published, not as your build ships them. All three are
partially compiled Angular libraries, and
esbuilddoes not run the Angular linker that a CLI build applies — which is what lets unused components drop out. That is the same for all three libraries, so the bundles compare fairly with each other; what one component costs in a real app is measured separately, on the CLI, in the code-splitting section. - Four bundles summed ≠ four components in one app. Real bundlers dedupe. The sums
above are upper bounds for Material and PrimeNG; the whole-library figure for
@guildofgleks/uihas no such inflation, which if anything works against it here. grepontypes/counts what is published, not what is reachable. Material splits some declarations into_*-chunk.d.tsfiles; the totals above are recursive over the wholetypes/directory precisely so nothing hides in a chunk, but a per-component grep (types/select.d.ts) will under-count for that reason.- Deprecation counts measure tags, not severity. One
@deprecatedon a whole component and one on an optional input count the same. - Nothing here measures runtime performance, accessibility conformance, or API quality. Those matter more than bytes for most teams, and none of them can be honestly reduced to a number produced by a shell command.