Adaptive “green UX” for the web. This Angular starter auto-tunes your UI for eco / balanced / pro modes based on real runtime signals (network quality, motion preference, device capability, brief CPU load). Ships with optional gadgets like mode-aware preloading, Speculation Rules toggling, and a smart image component.
- Context sensing:
prefers-reduced-motion,navigator.connection,deviceMemory,hardwareConcurrency, light “busy” hints. - Policy engine: maps signals →
eco | balanced | pro, setsbody[data-ux-mode]. - Directive:
modeClassadds BEM-style classes (card card--eco|balanced|pro) to any element. - Tokens: SCSS variables per mode (animation, elevation, image density).
- (Optional) Routing preloading: preload aggressively in pro, opt-in in balanced, never in eco.
- (Optional) Speculation Rules: inject
<script type="speculationrules">…</script>in pro only. - (Optional) Smart images:
\<smart-img>pickssrc/srcset, loading strategy, and priority per mode. - SSR-ready with browser-safety guards.
- Angular 17+ (standalone APIs, Signals)
- SCSS
- Vite dev server (default in Angular 17+)
- Optional SSR (Angular server rendering)
# 1) Clone
git clone https://github.com/<you>/ecoux-agent.git
cd ecoux-agent
# 2) Install deps
npm i
# 3) Run (SPA mode)
npm run start
# opens http://localhost:4200# Dev SSR + hydrate on client
npm run dev:ssrIf you prefer the classic zone-based mode, leave it as-is. If you want zoneless, see Zoned vs Zoneless below.
src/
├─ index.html
├─ main.ts # client entry (SPA or CSR hydration)
├─ main.server.ts # server entry (SSR only)
├─ styles.scss # global, imports styles/_modes.scss
├─ styles/_modes.scss # CSS variables for eco/balanced/pro
└─ app/
├─ app.config.ts # routes + (optional) preloading strategy
├─ app.component.* # demo + mode badge
├─ core/
│ ├─ runtime-context.service.ts # reads env signals (browser-guarded)
│ ├─ ux-agent.service.ts # policy -> mode + sets body[data-ux-mode]
│ ├─ mode-preloading.strategy.ts # (optional) route preloading per mode
│ └─ speculation.service.ts # (optional) Speculation Rules toggler
├─ shared/
│ ├─ mode-class.directive.ts # adds base/base--mode classes
│ └─ smart-img.component.ts # (optional) mode-aware images
└─ features/
└─ demo/
└─ demo-card/
├─ demo-card.component.ts
├─ demo-card.component.html
└─ demo-card.component.scss
- Sense (client only):
RuntimeContextServicesamples motion preference, connection, memory/cores, and brief long tasks. - Decide:
UxAgentServicemaps signals →eco | balanced | proand writesbody[data-ux-mode]. - Style: your SCSS reads the mode via CSS vars or
modeClass(card--eco|balanced|pro). - Enhance (optional): router preloading, Speculation Rules, and smart image priorities adapt to mode.
In any template:
<section modeClass [base]="'card'">
<h3>EcoUX Demo</h3>
<p>Mode: <strong>{{ ux.mode() }}</strong> ({{ ux.reason() }})</p>
<!-- Optional smart image -->
<smart-img
[alt]="'Hero'"
[sources]="{
'1': '/assets/hero@1x.jpg',
'1.5':'/assets/hero@1_5x.jpg',
'2': '/assets/hero@2x.jpg'
}">
</smart-img>
</section>SCSS:
.card { border-radius: 16px; padding: 16px; background: #fff; transition: box-shadow var(--anim-ms) ease; }
.card--eco { box-shadow: 0 4px 8px rgba(0,0,0,.06); }
.card--balanced { box-shadow: 0 12px 20px rgba(0,0,0,.08); }
.card--pro { box-shadow: 0 24px 36px rgba(0,0,0,.10); }Global tokens (styles/_modes.scss):
:root { --anim-ms: 180ms; --elev: 16px; --img-density: 1; }
body[data-ux-mode="eco"] {
--anim-ms: 0ms; --elev: 4px; --img-density: .75;
scroll-behavior: auto;
* { animation: none !important; transition-duration: 0ms !important; }
}
body[data-ux-mode="pro"] { --anim-ms: 140ms; --elev: 24px; --img-density: 1.5; }- Mode-aware route preloading: see
core/mode-preloading.strategy.tsand enable inapp.config.tswithwithPreloading(ModePreloadingStrategy). Usedata: { preload: true|false }per route. - Speculation Rules: injects/removes a
<script type="speculationrules">tag in pro. Activate by injectingSpeculationServiceonce (e.g., inAppComponent). - Smart images: use
\<smart-img>for mode-awaresrcset,loading, andfetchpriority.
This starter supports both. Choose one and be consistent.
-
Client (
main.ts) must import:import 'zone.js'; // first
-
Server (
main.server.ts) must import:import 'zone.js/node'; // first
-
Do not provide zoneless change detection.
-
Do not import Zone.js anywhere.
-
Client:
import { provideZonelessChangeDetection } from '@angular/core'; // or provideExperimental... on v17
-
Server:
import { provideServerRendering } from '@angular/platform-server';
-
Third-party libs that rely on zones may need adjustments.
If you see
Error: NG0908: Angular requires Zone.js, you’re mixing modes. Use one approach only.
-
ReferenceError: matchMedia is not defined(SSR/tests) Guard browser APIs withisPlatformBrowser(inject(PLATFORM_ID))and run setup inafterNextRender(...). -
Attribute modeClass is not allowed hereEnsuremodeClassdirective isstandalone: trueand imported in the component’simports: []. -
TS18046: c is of type unknownType tokens:const tokens: string[] = Array.from(el.classList);
-
TS2307: Cannot find module './features/...demo-card.component'File missing or wrong path/export. Confirm the file exists and exportsDemoCardComponent. In lazy routes, don’t include.tsextension. -
Unresolved variable or type uxInject the service in the component class that owns the template:ux = inject(UxAgentService);
-
NG0908: Angular requires Zone.jsImport'zone.js'(client) and'zone.js/node'(server) or switch to pure zoneless (and remove both imports). -
Vite cache weirdness after file adds Delete
node_modules/.viteand restart.
{
"scripts": {
"start": "ng serve",
"build": "ng build",
"dev:ssr": "ng run ecoux-agent:serve-ssr",
"build:ssr": "ng run ecoux-agent:build-ssr",
"prerender": "ng run ecoux-agent:prerender"
}
}Names may differ if your app name in
angular.jsonisn’tecoux-agent.
- Never ship API keys in the browser. If you later add an AI policy, call it via a tiny backend proxy.
- All browser-only logic is feature-detected and wrapped to be SSR-safe.
Issues and PRs welcome! Please:
- Keep browser-only code guarded (
isPlatformBrowser). - Add unit tests for policy changes where possible.
- Update README if you change public APIs (
modeClass,smart-img).
MIT
If you get stuck, open an issue with:
- Angular version (
ng version) - Your
main.ts,main.server.ts - The exact error + stack trace
Happy shipping! 🚀