A lightweight Chrome/Chromium extension that cycles through a list of URLs in separate tabs like a slideshow — perfect for dashboards, monitoring screens, TVs, info-kiosks, digital signage, reports, presentations, and more.
- Tabs Rotator / Slideshow
- Table of Contents
- Features
- Quick Start
- Usage
- Configuration Examples
- Advanced Settings
- Tips & Troubleshooting
- Privacy & Permissions
- Development
- Changelog
- Support
- Architecture
- Rotate through web pages from the Internet or local files (
file://links). - Per-page display time and auto-reload interval.
- Manage configuration directly in the extension UI or use import/export.
- Remote configuration URL with automatic periodic updates (host JSON anywhere).
- Built-in retry and temporary skip for failing pages.
- Persistence across browser restarts without tab spam.
- Offline-friendly: keeps the last good version if a page update fails.
- Optional Prevent window focus mode (keeps Chrome in background while rotating)
- Rotation watchdog self-heal (recovers if a rotate alarm is missed)
- Diagnostics panel with live state, timestamps & force actions
- One-click Force rotate now button for manual advancement
- Activation diagnostics & health badge (history, last success age, color-coded badge)
- Debug activation logging toggle (enable deep rotation logging only when you need it)
Screenshots:
- Install the extension (Chrome/Edge/Brave supported).
- Open the extension by clicking on it’s icon, then set Local Configuration or choose Remote Config (sample https://api.jsonsilo.com/public/e683a7af-7366-4db0-94fe-3438c9f64092).
- Add your pages (URLs) with a display time and optional reload interval.
- Start rotating (open the rotator and press Start). Press F11 to go fullscreen or set Start in Fullscreen Mode in configuration.
- To stop rotation, first exit from the full screen mode by pressing F11, then click on the extension icon and click Stop.
Local files? In
chrome://extensions/→ Tabs Rotator → enable Allow access to file URLs to usefile://links.
Configure pages directly inside the extension UI. You can export to JSON and import later or on another device.
Host a JSON file (JSON Silo, n:point, Pantry, S3, GitHub Pages, Gist, etc.) and paste its URL in the options. The extension fetches it on a schedule and applies updates without reinstalling.
- Configuration URL: HTTPS or
file://is supported. - Reload Interval (minutes): Set to
0to disable periodic refresh of the remote file.
A small public example is here:
tabs-rotator-config.json
| Name | Description | JSON Key | Type |
|---|---|---|---|
| URL | The page to display. https://… or file://…. |
url |
String |
| Display Time | Time in seconds the page stays visible before switching. | delaySeconds |
Number |
| Reload After | Page reload interval in seconds. Use 0 to disable reloading. |
reloadIntervalSeconds |
Number |
For automatic startup of Chrome on sign-in, follow the short guide: Windows Autostart (Chrome) — Windows 10/11.
{
"pages": [
{
"url": "https://status.example.com",
"delaySeconds": 20,
"reloadIntervalSeconds": 60
},
{
"url": "file:///C:/dashboards/report.html",
"delaySeconds": 15,
"reloadIntervalSeconds": 0
},
{
"url": "https://grafana.example.com/d/sales",
"delaySeconds": 30,
"reloadIntervalSeconds": 3600
}
],
"isFullscreen": true,
"preventWindowFocus": true
}{
"pages": [
{ "url": "https://example.com/one", "delaySeconds": 15, "reloadIntervalSeconds": 0 },
{ "url": "https://example.com/two", "delaySeconds": 20, "reloadIntervalSeconds": 120 }
],
"isFullscreen": true,
"preventWindowFocus": false
}These optional controls help tune behavior for signage / unattended scenarios:
- Prevent window focus (
preventWindowFocus): When enabled, tab rotation won’t bring the Chrome window to the foreground. Useful if you’re using the machine for something else while a dashboard rotates in the background (e.g. on a secondary display). - Start in Fullscreen Mode (
isFullscreen): Automatically requests fullscreen (F11 equivalent) after tabs are created. - Remote Reload Interval: Defines how often (in minutes) the remote JSON config is re-fetched. Set to
0to disable automatic refresh but still perform an initial load. - Force rotate (Diagnostics): Manually advances to the next page immediately; handy for testing or if you just updated a page.
- Watchdog Self-Heal: Internal mechanism that restarts rotation if the scheduled alarm was missed (no UI toggle; always on).
- reuseLocalFileTabs (
reuseLocalFileTabs): Whentrue,file://pages use legacy in-place reload (no hidden preload). Whenfalse(default / omitted) they behave like normal web pages and get a hidden preload tab first for faster promotion of heavy local dashboards.
Minimal example with advanced keys:
-
Local files don’t load (
file://)
Enable Allow access to file URLs for the extension underchrome://extensions/. To opt back into the older in-place reload behavior for local files, add"reuseLocalFileTabs": trueto your configuration JSON. -
Some sites log out after a while
Use the site’s "keep alive"/auto-refresh, increase per-pagereloadIntervalSeconds, or keep a logged-in session. -
A page fails to load
The rotator will retry and temporarily skip failing pages. Check DevTools (F12) → Console for errors. -
Fullscreen / Kiosk
Set "Start in Fullscreen Mode" in configuration. For signage, consider Chrome’s kiosk mode or an OS auto-start to open the browser and extension on boot. -
Tabs keep multiplying
The extension persists state across restarts and prevents tab spam and removes all opened tabs on stop. If something seems off, stop rotation, close tabs, and start again. -
Need to advance immediately
Open Diagnostics → click Force rotate to move to the next page instantly. -
Rotation stalled / stopped at last tab
The watchdog typically restarts it automatically. Open Diagnostics to confirm timestamps. Use Force rotate if needed. Supports activation history + badge color to help spot issues.
- Uses only the minimal permissions necessary to open/rotate tabs and store your settings locally.
- No analytics, tracking, or external calls unless you opt-in by configuring a Remote Configuration URL.
- Remote configs are fetched read-only; the extension never writes data back to remote services.
We also test the inverse: when auto-preserve is explicitly disabled we expect the simulated restart to recreate rotation tabs (new tab IDs). The spec e2e/tests/non-preserve-reload.spec.ts calls:
__e2eApi.disableAutoPreserve();
// then invokes simulateServiceWorkerRestart(context)Negative test support API:
| Method | Purpose |
|---|---|
disableAutoPreserve() |
Sets DisableAutoPreserveNextInit so next init does not preserve existing tabs |
Preserve logic summary:
| Storage flag | Effect |
|---|---|
forcePreserveNextInit |
Internal flag: if true (and disable flag not set) next initialize() uses preserveExisting |
disableAutoPreserveNextInit |
Internal flag set by disableAutoPreserve(); next init will not preserve tabs |
In production builds (NODE_ENV=production) the entire __e2eApi block is stripped by the webpack DefinePlugin guard (__E2E_TESTING__ becomes false), reducing bundle surface area.
Playwright exercises a full extension lifecycle, including a simulated MV3 service worker restart ("crash") while preserving existing rotation tabs. Real MV3 runtime reload detection in headless Chromium is unreliable; the test harness uses simulateServiceWorkerRestart(context) to re-run rotationService.initialize with the appropriate preservation option instead of relying on a new Worker instance.
Scripts:
| Command | Purpose |
|---|---|
yarn e2e |
Build extension then run headless Playwright tests |
yarn e2e:headed |
Run tests in headed Chromium |
yarn e2e:debug |
Launch with Playwright inspector (PWDEBUG) |
First time only install the browser (if not already):
npx playwright install chromiumKey test files:
e2e/tests/crash-recovery.spec.ts: preserved resume scenarioe2e/tests/non-preserve-reload.spec.ts: non-preserve negative scenario
Helper: simulateServiceWorkerRestart(context) in e2e/utils/launch-extension.ts.
The E2E Playwright tests are intended for local development only and are not executed in CI (CI builds and runs unit tests + lint + coverage). This avoids flakiness with MV3 extension loading in headless environments.
Run locally (headed Chromium):
yarn e2eOr with the inspector for debugging:
yarn e2e:debugFor reliability we bypass chrome.runtime.sendMessage and talk directly to the service worker global via page.serviceWorker().evaluate(...). The background script exposes a non-production API object:
__e2eApi = {
startWithConfig(config),
getDiagnostics(),
getState(),
listTabs(),
forceHeartbeat(),
adoptTabs(),
// crash() was replaced by simulateServiceWorkerRestart(context) harness helper for reliability (MV3 reload detection is flaky headless)
}ForcePreserveNextInit is a storage flag that causes the next initialize() call after a worker reload to adopt existing tab IDs instead of recreating tabs. The crash recovery test asserts that tab IDs survive across a simulated reload.
- Create a spec in
e2e/tests/*.spec.ts. - Launch via one of the scripts above.
- Use
serviceWorker.evaluateto call into__e2eApi.
The suite exercises rotation progression, reload cadence, fullscreen/focus modes, error recovery & retries, remote config reload, import/export, preservation boundaries, crash / non‑preserve restart, tab adoption and pruning, startup race handling, watchdog self‑heals (stall + missing alarms), cleanup stop path, metrics/large‑scale behavior, cross‑window isolation, storage failure resilience, and grace period enforcement. Heavy scenarios (scale, metrics snapshot growth, cross‑window) are tagged with @heavy and executed in a single‑worker project to reduce resource spikes.
Reliability helpers (in e2e/utils/reliability-helpers.ts) eliminate flakiness by waiting on explicit internal signals rather than arbitrary sleeps:
waitForWorkerApi(context) // service worker & __e2eApi ready
waitForTabIds(context, count) // rotation tabs created
waitForIndex(context, idx) // current index observed
waitForRotationCycle(context, n) // rotation cycle counter reached
pollAlarms(context) // list current alarm namesUse these instead of manual polling loops for consistency.
Minimal non‑production test surface is exposed behind a build flag. It lets tests start rotation, inspect diagnostics/state, advance indices, simulate errors, adjust watchdog/grace timing, and export/update config. For exact method names see the background script (__e2eApi definition). Production builds strip this entire block.
| Symptom | Tip |
|---|---|
| Test hangs waiting for rotation | Ensure delaySeconds >= 3 (validator) and config uses StorageKeys.LocalConfig. |
| Tabs not preserved after crash | Confirm forceHeartbeat(), adoptTabs(), then crash() were invoked; check resumeReason in diagnostics. |
| SW not ready | Open popup chrome-extension://<id>/index.html or poll for __e2eApi. |
Run a full multi-target type check:
yarn typecheckThis validates app code (tsconfig.app.json), background code (tsconfig.background.json), and harness (tsconfig.harness.json). E2E tests compile via Playwright + their own tsconfig (tsconfig.e2e.json).
Requirements: Node.js ≥ 18, npm and yarn.
-
Install dependencies
yarn
-
Build the extension (produces a clean
dist/and a zip bundle)yarn build
-
Run in watch mode for local development
yarn start
-
Load the unpacked extension
- Open
chrome://extensions/ - Enable Developer mode (top-right)
- Click Load unpacked and choose the subfolder inside
dist/created by the build
- Open
Tip: If you change background or options code, the service worker may need a manual reload in
chrome://extensions/during development.
Two layers:
- Background (Node + Jasmine) – rotation & tab logic.
- Angular UI (Karma + Jasmine) – component/service specs.
yarn test:bgWatch:
yarn test:bg:watchyarn test:uiWatch:
yarn test:ui:watchyarn test:allWatch both:
yarn test:all:watch| Layer / Purpose | tsconfig | Script(s) | Notes |
|---|---|---|---|
| Angular UI unit/shared | tsconfig.spec.app.json |
yarn test:ui |
Karma + ChromeHeadless |
| Background rotation logic | tsconfig.background.spec.json |
yarn test:bg |
Jasmine (Node, CommonJS build) |
| Playwright E2E (extension) | tsconfig.e2e.json |
yarn e2e, yarn e2e:debug |
MV3 lifecycle + full integration |
| Production UI build | tsconfig.app.json |
yarn build / ng build |
Excludes all *.spec.ts |
| Production background SW | tsconfig.background.json |
Part of yarn build |
Excludes tests directory |
| Harness / manual scripts | tsconfig.harness.json |
node dist/harness/*.js | Exploratory / tab-manager harness |
| Tooling / configs | tsconfig.tools.json |
(editor only, no emit) | Playwright config + scripts |
Naming Conventions:
- Background tests use
*.bg-spec.tsto avoid accidental production inclusion. - UI tests use
*.spec.tsundersrc/apporsrc/shared. - E2E tests live under
e2e/tests/*.spec.ts.
Adding a New Test:
- Choose layer (UI / background / E2E).
- Use correct suffix (
.spec.tsor.bg-spec.ts). - Run corresponding script.
If a test is not picked up:
- Verify path matches its tsconfig
include. - Confirm naming convention is correct.
- For background, rebuild with
yarn test:bg:buildif needed.
Troubleshooting:
| Symptom | Cause | Fix |
|---|---|---|
| No specs found (bg) | Not compiled / wrong dir | yarn test:bg:build then re-run |
| UI hang | Headless Chrome missing | Adjust script or install Chrome |
| chrome types leak | Background file imported into UI | Move shared models to src/app/models |
| Stale bg code | Forgot rebuild | Use yarn test:bg or watch |
The repository uses a multi-tsconfig layout to isolate concerns and remove accidental Chrome ambient types from Node / test contexts:
tsconfig.base.json– Shared compiler defaults (no implicit chrome types).tsconfig.app.json– Angular UI build (extends base). Does not include chrome types unless UI code needs them.tsconfig.background.json– Service worker / background scripts; addstypes: ["chrome-types"]so the global namespace is available only here.tsconfig.harness.json– Node harness for logic testing; onlynodetypes by default (can add a stub orchromeif needed).- Root
tsconfig.json– Project references orchestrator so VS Code understands all sub-projects.
Benefits:
- Faster, clearer editor IntelliSense (each file maps to an appropriate project).
- Prevents leaking global
chromenamespace into harness/tests accidentally. - Easier future migration or update when Chrome APIs change (using
chrome-types).
If you add a new background file under src/background/, it is automatically picked up by tsconfig.background.json. For UI-only shared models, keep them in src/app/models/ so both app and background can import without pulling in Angular-specific code.
If you later need Chrome APIs in the UI (rare), add
"types": ["chrome-types"](or a minimal stub) totsconfig.app.json.
GitHub Actions workflow (.github/workflows/ci.yml) runs on pushes & pull requests targeting main.
Pipeline steps:
- Checkout & cache dependencies.
yarn typecheck(background + UI TS integrity).yarn test:bg(background Jasmine specs).yarn test:ui(Karma/ChromeHeadless UI specs).- Build & upload extension artifact (optional).
- Upload compiled background spec output for debugging.
Key scripts:
| Script | Purpose |
|---|---|
yarn test |
Full suite (test:all). |
yarn test:bg |
Background specs. |
yarn test:ui |
UI specs. |
yarn test:all |
Background then UI. |
yarn test:bg:watch |
Watch background specs. |
yarn test:ui:watch |
Watch UI specs. |
Coverage (current status):
- UI tests: Karma is configured; enable coverage via
ng test --code-coverage(add a script liketest:ui:coverage). - Background tests: Add NYC + source maps to instrument TS (e.g.,
nyc --reporter=lcov yarn test:bg).
Example UI coverage run:
ng test --watch=false --code-coverageTo add background coverage:
yarn add -D nyc source-map-support
nyc --reporter=lcov --reporter=text-summary yarn test:bgOptionally merge coverage reports and upload to Codecov:
yarn install -D codecov
codecov -f coverage/lcov.infoThe styling stack uses Tailwind CSS v4 directives with a split preflight file to avoid @import ordering warnings and centralize design tokens.
Core files:
| File | Purpose |
|---|---|
src/tailwind.preflight.css |
Contains all Tailwind directives: @source, @theme tokens, @import "tailwindcss", @plugin, @tailwind utilities. |
src/styles.source.css |
Authoring source: imports preflight then declares custom @utility classes and app base styles. Snapshot expands this. |
Key directives:
@source– Replaces the oldcontentarray (scan paths for class extraction).@theme {}– Inlined design tokens (colors, spacing, breakpoints, typography) used to generate utilities.@plugin– Registers extra plugin layers (e.g. forms).@tailwind utilities– Forces expansion of the utilities layer (explicit for some build chains).@utility– Define bespoke one-off utilities (e.g.animate-spin-slow).
Linting:
stylelint is configured (see .stylelintrc.json) to allow Tailwind v4 at-rules: @source, @theme, @plugin, @tailwind, @utility.
Run CSS lint:
yarn lint:cssExtending design tokens:
- Edit the
@themeblock insrc/tailwind.preflight.css. - Add any new custom utility via
@utilityinsrc/styles.source.css. - Re-run
yarn start(watch) oryarn buildto ensure new classes generate.
If your editor flags unknown at-rules, enable a Tailwind-aware extension; the build and Stylelint already accept them.
Remember to exclude out-tsc/ from instrumentation (instrument sources, not compiled output).
The build uses a pre-generated Tailwind snapshot to ensure deterministic utilities. Directives are expanded by the prebuild step from src/styles.source.css (which imports tailwind.preflight.css) into src/styles.tailwind.css, which Angular consumes.
When you need to inspect or diff the fully expanded utilities (e.g. after upgrading Tailwind), generate a local snapshot:
yarn tailwind:generateThis writes src/styles.tailwind.css (gitignored & lint-ignored). Open it to see all emitted layers and utilities. Do not edit the snapshot directly—edit styles.source.css and re-run.
For the full change log, open this file: Changelog.
- Found a bug or have a feature request? Open an issue: https://github.com/skarpovru/chrome-tabs-rotator/issues
If you enjoy the project, a ⭐ on GitHub helps others find it!
The extension is built as a Manifest V3 (service worker) + Angular UI hybrid. Modularized rotation logic into focused services to improve maintainability, testability, and observability.
rotation.service.ts– Thin coordinator (start/stop, delegations, high-level flow).config.service.ts– Local + remote configuration loading & periodic refresh.rotation-scheduler.service.ts– Computes/schedules next rotation alarm; starts countdown; updates health timing.activation.service.ts– Resilient tab activation + diagnostics + focus orchestration hooks.rotation-watchdog.service.ts– Missed-alarm / stall watchdog; triggers enforced rebuild + forced rotation.startup-recovery.service.ts– Rehydrates state & alarms after MV3 service worker restart.rotation-state.facade.ts– Canonical rotation state mutation (index advance, normalization, persistence, toolbar sync).rotation-state.repository.ts– Persistence adapter for rotation state.tab-manager.service.ts– Tab creation/tracking (primary + preloaded), allowed ID set, metrics logging.tab-lifecycle.service.ts– Preload strategy + reload alarm handling + initial load wait.invariant-rebuilder.service.ts– Idempotent rebuild & pruning of orphan/duplicate tabs.stall-guard.service.ts– Stall heuristics (overdue rotation, cyclic last-page) and recovery signaling.health-monitor.service.ts– Tracks timing & stall inputs → severity + badge color.countdown.service.ts– Badge countdown timer overlay.activation-diagnostics.service.ts– Activation attempt ring buffer & last success/error.focus-orchestrator.service.ts– Fullscreen + optional focus suppression.focus.service.ts– Low-level window focus attempt wrapper.scheduler.service.ts– Low-level Chrome alarm helpers.diagnostics.service.ts– Aggregates snapshot (tabs, timing, health, stalls, activation).metrics.service.ts– Counter tracking + runtime broadcast.storage.service.ts– Promise API + small TTL cache overchrome.storage.local.
rotation.service.ts deliberately stays thin; logic with its own state or heuristics lives in a dedicated service for clarity and testability.
Badge color summarizes overall rotation health, not just the last activation:
- Inputs: timing (next due vs now), stalls, activation failure density, any recent success.
- Severity: normal → warn (overdue or stall) → error (persistent failures / multiple stalls / no success yet).
- Colors: green / amber / red; countdown time remains but is overridden by degraded severity.
- Rebuild is idempotent: only runs if
tabsConfigabsent (typical after MV3 worker restart) – avoids unnecessary tab enumeration. - Placeholder handling: missing tab IDs are lazily recreated on demand during rotation, preventing deadlocks at a single index.
- Watchdog self‑heal: detects overdue rotations, triggers fast rebuild + invariant enforcement, and forces a near‑term rotate alarm.
- Invariant enforcement centralized so stale / orphaned tabs are pruned without duplicating logic in rotation flow & watchdog.
Diagnostics payload now includes:
severity&badgeColor(composite health view)- Activation history (recent attempts) & last activation success age
- Stall metadata (count, reason, timestamps)
- Normalized tracked vs. allowed tab IDs for spotting leaks
This richer snapshot enables a UI to present immediate actionable state without recomputing heuristics.
- Single Responsibility – Each new service does one thing (preload/reload, focus, health, rebuild) → easier isolated tests.
- Crash/Restart Resilience – Service worker wakeups should not require reinitializing or duplicating logic; rebuild + invariant layers guarantee a safe baseline.
- Observability First – All critical transitions (activation attempt, stall, rotation schedule) feed diagnostics or metrics quickly, supporting live panels.
- Minimal Global State – Persistent state limited to
RotationState+ derived indexes; everything else is recomputable or ephemeral.
Supporting models live in src/app/models/ and remain framework-agnostic so they can be safely imported from the background service worker context.
- User clicks Start →
RotationService.initialize()loads config, clears stale alarms, resets state. - Tabs are created through
TabManagerService(first tab active, others preloaded as needed). - Each rotation cycle schedules the next alarm (
rotate) based on the current page’sdelaySeconds. - On each alarm,
RotationService.rotateTabs()activates the next tab (or swaps in a preloaded nextTab) and records timing. StallGuardServiceevaluates post-rotation and watchdog ticks; if a stall is detected, it triggers a controlled reset.DiagnosticsServicecomposes real‑time status for the UI (Force Rotate, Enforce Now, etc.).
To avoid “tab spam,” only up to 2 tabs per configured page (active + preloaded) are allowed. Extras (e.g. from restarts or failed preloads) are pruned during rotation and watchdog ticks. Ordering of tracked IDs is normalized so rebuilds correctly map page indices.
When preventWindowFocus is enabled, focus attempts are intentionally suppressed but still logged for diagnostics transparency. Fullscreen entry respects the setting (fullscreen without re‑focusing).
Potential next modular slices:
- Persistence layer abstraction for storage calls
- Metrics/event bus for streaming diagnostics updates
- Optional in-memory cache layer atop StorageService for high-frequency reads
This architecture aims to keep RotationService as a thin coordinator while enabling isolated unit tests around logic-heavy concerns (tab lifecycle, stall detection, diagnostics shaping).
A lightweight MetricsService tracks counters:
rotationsstallsfocusAttemptstabCreationsreloadsScheduled
Each mutation emits a runtime message { kind: 'metrics', event: { type, counters, at } } consumed by the diagnostics panel via a chrome.runtime.onMessage listener. This removes the need for high‑frequency polling; the panel still supports manual/interval refresh for full snapshots while the top bar of live counters updates instantly.
StorageService now includes an optional, very small TTL (default 5s) in‑memory cache to reduce repetitive chrome.storage.local.get calls for hot keys (rotationState, configs, flags). The cache auto‑invalidates on set / remove. TTL is intentionally conservative to avoid stale UI while still smoothing bursty access patterns from rotation + diagnostics requests.
If stricter consistency is required, the TTL can be set to 0 when constructing StorageService to disable caching.


{ "pages": [{ "url": "https://example.com/dashboard", "delaySeconds": 20, "reloadIntervalSeconds": 300 }], "isFullscreen": false, "preventWindowFocus": true, "reuseLocalFileTabs": false }