An interactive, ThoughtWorks-inspired technology radar for the Signifly dev team. It shows what we're adopting, trialing, assessing and holding — and, more importantly, the reasoning behind each call.
Live: https://tech-radar-one.vercel.app · content is managed in DatoCMS.
Built lean: Next.js 16 · React 19 · TypeScript · Zod · Tailwind v4. The radar visualisation and its animated background are pure Canvas 2D — no D3, no charting library, no extra dependencies.
pnpm install
cp .env.local.example .env.local # add your DATOCMS_API_TOKEN (see below)
pnpm dev # http://localhost:3000
pnpm build # dynamic SSR app, deployed on VercelA blip is one technology, tool or technique. Every blip has:
| field | values |
|---|---|
quadrant |
languages-frameworks · platforms · tools · techniques |
ring |
adopt · trial · assess · hold (centre → edge) |
movement |
new · in · out · none |
description |
short rationale — the why, the learning |
tags |
optional, for filtering/search later |
- Rings encode confidence. The closer to the centre, the more we reach for it by default.
- Quadrants group by kind. Each has an accent colour used consistently across the UI.
- Movement is drawn on the blip (a triangle = new; chevrons = moved in/out).
Everything human-facing (labels, colours, ring blurbs, geometry) lives in
lib/radar.ts. Data only ever references the stable slugs, so you can
rename or recolour a quadrant without migrating content.
The radar reads its blips live from DatoCMS on every request. The single boundary
between the UI and the data is lib/content.ts.
- Model:
radar_blip(GraphQLallRadarBlips), with fieldsname,slug,quadrant,ring,movement,description,tags.slugmaps to the blip'sid. - Rendering: the page is fully dynamic —
export const dynamic = "force-dynamic"inapp/page.tsxandcache: "no-store"on the fetch. Publish a change in DatoCMS and it appears on the next page load; nothing to rebuild. - Fallback: if
DATOCMS_API_TOKENis unset, or the CMS is unreachable,getBlips()transparently falls back to the typed seed incontent/blips.ts, so local dev and previews never render an empty radar. Everything is validated against the Zod schema inlib/schema.ts, so bad data fails fast.
Add or move a blip in DatoCMS and publish — that's it. Positions are derived
deterministically from the slug, so blips never jump around between renders.
DATOCMS_API_TOKEN=… # DatoCMS read API token (kept in .env.local, gitignored)
DATOCMS_DRAFT=true # optional: include draft (unpublished) blips in previewsOn Vercel the token is set for Production, Preview and Development.
Performance note: fully dynamic means every visit makes a live CMS call. For a small internal tool that's fine. To get fresh-but-cached later, switch to tag-based caching + a DatoCMS publish webhook that hits a revalidate route.
components/RadarCanvas.tsx— the radar: ring circles, quadrant axes, an animated sweep, round blips, hit-testing, hover/click, and an eased zoom camera (click a quadrant to fly into it, click a blip to zoom to it, Esc to pull back). Respectsprefers-reduced-motion.components/MeshBackground.tsx— the background: a pixel "landscape" that parallax-pans with inertia, gradient-masked to fade at the edges, and subtly reactive to cursor movement and clicks.components/RadarApp.tsx— the shell: floating brand lockup, quadrant/ring filters, the selected-blip detail, and a synced blip index (hovering the list highlights the radar and vice-versa).
The visualisation is v1; the process around it is what keeps a radar honest:
- Propose — draft a new/moved blip in DatoCMS (or open a PR editing the seed) with a one-paragraph rationale.
- Discuss — the team reviews at a regular radar session; a blip only moves ring on rough consensus.
- Publish — publish in DatoCMS and it's live immediately. Cut a dated "volume" periodically so movement stays meaningful over time.