technical deep-dive

Building 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:

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:

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:

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

User drops image ↓ POST /api/upload → Validates file type and size → Streams buffer to Cloudinary → Returns secure_url ↓ POST /api/analyse → Azure Computer Vision ─┐ → Node Vibrant ─┤ parallel ↓ → Merge results → Return { colorScheme, palette } ↓ Frontend renders palette → Stores result in localStorage

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:

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.