technical deep-dive
May 12, 2026Building Colourfully: AI-Powered Colour Palette Extraction with Next.js, Azure, and Cloudinary.
Colour is one of the most essential elements in design and photography.
It sets mood, communicates brand, and guides attention. So when I set
out to build Colourfully, a tool that extracts and analyses the colour
palette of any image, I wanted it to go deeper than a simple
pixel-counter. I wanted it to think about colour the way a designer
would.
Here's a technical deep-dive into how it all came together.
What Colourfully Does
Colourfully lets you upload an image and instantly receive:
- Dominant colour analysis: foreground, background, and accent colours identified by AI
- Full palette extraction: every meaningful colour in the image, with RGB values and relative prominence
- Persistent results: your last analysis is saved locally so you can revisit it without re-uploading.
The interface is intentionally minimal: drag your image in, and the palette appears.
Tech Stack
The project runs on a modern, production-grade stack:
| layer | technology |
|---|---|
| Framework | Next.js 15.3 with React 19 |
| Language | TypeScript 5 |
| Styling | Tailwind CSS 4.1 |
| Animations | Framer Motion 12 |
| Image hosting | Cloudinary 2.6 |
| AI (colour scheme) | Azure Computer Vision |
| Palette | Node Vibrant 4.0 |
| Deployment | Vercel |
Next.js 15 ships with the App Router and React Server Components, which means I get file-system routing, server-side API logic, and frontend rendering all in one cohesive mental model. Turbopack handles dev builds, which keeps the feedback loop tight during development.
The Two-Layer AI Strategy
The most interesting architectural decision in Colourfully is the dual-approach to colour analysis. Rather than relying on a single service, the app runs two complementary tools in parallel and merges their results.
Layer 1: Azure Computer Vision
Microsoft's Computer Vision API handles the semantic side of colour analysis. It doesn't just count pixels, it understands the structure of an image: what's foreground, what's background, and what the most eye-catching accent colour is.
The integration lives in src/app/utils/azure.ts. The client is
initialised with Cognitive
Services credentials:
const computerVisionClient = new ComputerVisionClient(
new ApiKeyCredentials({ inHeader: { 'Ocp-Apim-Subscription-Key': key } }),
endpoint
);
And the actual call requests only the Colour
visual feature to keep latency low:
const result = await computerVisionClient.analyzeImage(imageUrl, {
visualFeatures: ['Color']
});
This returns structured data:
dominantColorForegrounddominantColorBackgroundaccentColor
The three colours a designer actually cares about when reviewing a palette.
One practical detail: Azure requires HTTPS URLs, so the code validates this before making the request. This is where Cloudinary becomes essential.
Layer 2: Node Vibrant
While Azure gives you the semantic picture, Node Vibrant gives you the complete one. It runs a full palette extraction pass over the image and returns named swatches:
VibrantMutedDarkMutedLightVibrant
and so on. Each with RGB values and a population count indicating how much of the image that colour occupies.
const palette = await Vibrant.from(imageUrl).getPalette();
// Returns: { Vibrant: { rgb: [r, g, b], population: n }, ... }
The two results are combined in the /api/analyse route and returned to
the frontend as a single unified payload. Azure tells you what colour means in the image; Vibrant tells you everything
that's there.
Cloudinary Integration
Before any analysis can happen, the image needs to live somewhere on the internet with a stable HTTPS URL. That's Cloudinary's job.
The upload flow (/api/upload) receives the image as a FormData payload,
converts it to a
Node.js Buffer, and streams it directly to Cloudinary:
const result = await new Promise<CloudinaryUploadResult>((resolve, reject) => {
cloudinary.uploader.upload_stream(
{ resource_type: 'image' },
(error, result) => {
if (error || !result) return reject(error);
resolve(result);
}
).end(buffer);
});
Streaming the buffer (rather than writing to disk) keeps the serverless function stateless and avoids
unnecessary I/O. Cloudinary returns a secure_url, a fully CDN-served
HTTPS image URL, which is then passed directly to the analysis endpoints.
I also configured Cloudinary's domain in next.config.ts so Next.js's
<Image> component can optimise Cloudinary-hosted images with
automatic resizing and format conversion.
Performance Characteristics
Parallel API Calls
The /api/analyse route fires both the Azure and Vibrant calls
simultaneously rather than waiting for one before starting the other. This cuts the round-trip time roughly in half for the most latency-sensitive part of the app.
localStorage Caching
Analysis results are persisted to localStorage on completion. If you
close the tab and
come back, your palette is still there. For a tool people use iteratively while working on a design,
this eliminates a lot of unnecessary re-uploads.
localStorage.setItem("analysisResult", JSON.stringify(analysisResult));
Canvas Animation
The home page features a vortex particle animation. 1,000–2,000 particles driven by simplex noise,
rendered on a <canvas> element with requestAnimationFrame. The
particle state is stored in Float32Array buffers for memory efficiency.
Despite looking
visually rich, it stays off the main thread and doesn't block the UI.
Upload Validation
Files are validated client-side before upload: JPEG, PNG, and WebP only, with a 5MB cap. This prevents unnecessary round-trips to the server for files that would fail anyway.
Authentication with Clerk
Auth is handled by Clerk, integrated at the root layout level. The middleware is configured but permissive, you don't need an account to use Colourfully. Authenticated users get a user menu; unauthenticated visitors get Sign In / Sign Up buttons. The analysis feature is fully public.
This was a deliberate choice as friction is the enemy of tools like this. If someone wants to extract a palette, they should be able to do it immediately.
Analytics
Vercel Analytics is wired into the root layout with a single component:
<Analytics />
That's all. On a Vercel deployment, this gives automatic tracking of page views, route transitions, and Core Web Vitals (LCP, CLS, FID), with no configuration required.
Architecture at a Glance
The API surface is small by intention: two routes, clearly separated concerns, typed inputs and outputs throughout.
Comparing the Two Services
| Azure Computer Vision | Node Vibrant | |
|---|---|---|
| Type | Cloud API (paid) | Open-source library |
| Output | Semantic colour roles | Full spectrum palette |
| Speed | Network-bound | CPU-bound (fast) |
| Requires URL | Yes (HTTPS) | Yes |
| Best for | Foreground/background/accent | Complete palette extraction |
| Cost | Per-call pricing | Free |
These two are complementary. Azure tells you which colours matter and why; Vibrant tells you all the colours that exist. Together they give a complete picture.
What I'd Build Next
A few directions worth exploring:
- Export formats: download the palette as a CSS custom properties file, a Figma token JSON, or an Adobe Swatch
- Side-by-side comparison: analyse two images and diff their palettes
- History: persist multiple past analyses with Clerk's user accounts, not just the most recent one
- Palette naming: use an LLM to generate a human-readable name for the palette ("Coastal Dusk", "Industrial Steel")
Conclusion
Colourfully is a small project, but it touches a lot of real-world concerns: cloud storage, AI APIs, authentication, performance optimisation, and a polished UI.
The tech choices were driven by pragmatism. Cloudinary for reliable image hosting, Azure for semantic intelligence, Vibrant for completeness, Clerk for auth without infrastructure overhead, and Vercel for deployment without ops.
If you want to extract the palette from an image right now, give it a try at colourfully.vercel.app.