ColorAssembly
Frameworks

Next.js Integration

Using ColorAssembly with Next.js App Router and Pages Router

Complete guide for integrating ColorAssembly in Next.js applications (App Router & Pages Router).

Installation

npm i@CymbolTech/colorassembly

Configuration

Add WASM support to next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack: (config) => {
    config.experiments = {
      ...config.experiments,
      asyncWebAssembly: true,
    };
    return config;
  },
};
 
module.exports = nextConfig;

App Router (Next.js 13+)

Client Component Pattern

// components/ColorConverter.tsx
'use client';
 
import { useState, useEffect } from 'react';
import { loadColorAssembly, type ColorAssemblyAPI } from '@cymbol/colorassembly';
 
export function ColorConverter() {
  const [api, setApi] = useState<ColorAssemblyAPI | null>(null);
  const [hex, setHex] = useState('#6366f1');
 
  useEffect(() => {
    loadColorAssembly().then(setApi);
  }, []);
 
  if (!api) return <div>Loading...</div>;
 
  const rgb = api.transforms.hexToRgb({ hex });
  const hsl = api.transforms.rgbToHsl(rgb);
 
  return (
    <div className="space-y-4">
      <input
        type="color"
        value={hex}
        onChange={(e) => setHex(e.target.value)}
        className="w-full h-12"
      />
      <div>
        <div>RGB: {rgb.r}, {rgb.g}, {rgb.b}</div>
        <div>HSL: {hsl.h.toFixed(0)}°, {hsl.s.toFixed(0)}%, {hsl.l.toFixed(0)}%</div>
      </div>
    </div>
  );
}

Context Provider Pattern

Create a provider for app-wide access:

// providers/ColorAssemblyProvider.tsx
'use client';
 
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
import { loadColorAssembly, type ColorAssemblyAPI } from '@cymbol/colorassembly';
 
const ColorAssemblyContext = createContext<ColorAssemblyAPI | null>(null);
 
export function ColorAssemblyProvider({ children }: { children: ReactNode }) {
  const [api, setApi] = useState<ColorAssemblyAPI | null>(null);
 
  useEffect(() => {
    loadColorAssembly().then(setApi);
  }, []);
 
  if (!api) {
    return (
      <div className="flex items-center justify-center h-screen">
        <div className="text-center">
          <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
          <p>Loading ColorAssembly...</p>
        </div>
      </div>
    );
  }
 
  return (
    <ColorAssemblyContext.Provider value={api}>
      {children}
    </ColorAssemblyContext.Provider>
  );
}
 
export function useColorAssembly() {
  const context = useContext(ColorAssemblyContext);
  if (!context) {
    throw new Error('useColorAssembly must be used within ColorAssemblyProvider');
  }
  return context;
}

Add to root layout:

// app/layout.tsx
import { ColorAssemblyProvider } from '@/providers/ColorAssemblyProvider';
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <ColorAssemblyProvider>
          {children}
        </ColorAssemblyProvider>
      </body>
    </html>
  );
}

Server Actions with ColorAssembly

While ColorAssembly is client-side, you can use it with server actions:

// app/colors/page.tsx
'use client';
 
import { useState } from 'react';
import { useColorAssembly } from '@/providers/ColorAssemblyProvider';
 
export default function ColorsPage() {
  const api = useColorAssembly();
  const [result, setResult] = useState<string>('');
 
  async function generatePalette(formData: FormData) {
    const hex = formData.get('color') as string;
    const rgb = api.transforms.hexToRgb({ hex });
    const palette = api.generate.semanticPalette(rgb, 'light');
    
    // Could send to server action for saving
    setResult(JSON.stringify(palette, null, 2));
  }
 
  return (
    <form action={generatePalette} className="space-y-4">
      <input type="color" name="color" defaultValue="#6366f1" />
      <button type="submit" className="px-4 py-2 bg-primary text-white rounded">
        Generate Palette
      </button>
      {result && <pre className="bg-gray-100 p-4 rounded">{result}</pre>}
    </form>
  );
}

Pages Router (Next.js 12)

Pages with getStaticProps

// pages/colors/[id].tsx
import { GetStaticPaths, GetStaticProps } from 'next';
import { useEffect, useState } from 'react';
import { loadColorAssembly, type ColorAssemblyAPI } from '@cymbol/colorassembly';
 
interface Props {
  colorHex: string;
}
 
export default function ColorPage({ colorHex }: Props) {
  const [api, setApi] = useState<ColorAssemblyAPI | null>(null);
 
  useEffect(() => {
    loadColorAssembly().then(setApi);
  }, []);
 
  if (!api) return <div>Loading...</div>;
 
  const rgb = api.transforms.hexToRgb({ hex: colorHex });
  const hsl = api.transforms.rgbToHsl(rgb);
 
  return (
    <div>
      <div
        className="w-full h-48 rounded-lg"
        style={{ backgroundColor: colorHex }}
      />
      <div className="mt-4">
        <p>RGB: {rgb.r}, {rgb.g}, {rgb.b}</p>
        <p>HSL: {hsl.h.toFixed(0)}°, {hsl.s.toFixed(0)}%, {hsl.l.toFixed(0)}%</p>
      </div>
    </div>
  );
}
 
export const getStaticPaths: GetStaticPaths = async () => {
  return {
    paths: [
      { params: { id: 'red' } },
      { params: { id: 'blue' } },
      { params: { id: 'green' } },
    ],
    fallback: true,
  };
};
 
export const getStaticProps: GetStaticProps = async ({ params }) => {
  const colorMap: Record<string, string> = {
    red: '#ff0000',
    blue: '#0000ff',
    green: '#00ff00',
  };
 
  return {
    props: {
      colorHex: colorMap[params?.id as string] || '#000000',
    },
  };
};

Custom _app.tsx

// pages/_app.tsx
import type { AppProps } from 'next/app';
import { ColorAssemblyProvider } from '@/providers/ColorAssemblyProvider';
 
export default function App({ Component, pageProps }: AppProps) {
  return (
    <ColorAssemblyProvider>
      <Component {...pageProps} />
    </ColorAssemblyProvider>
  );
}

API Routes

While ColorAssembly is WASM-based (browser-only), you can create API routes for color-related operations:

// app/api/colors/route.ts
import { NextRequest, NextResponse } from 'next/server';
 
// This would need a Node.js compatible color library
// or return data for client-side ColorAssembly processing
 
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const hex = searchParams.get('hex');
 
  // Return color data for client to process
  return NextResponse.json({
    hex,
    // Client will use ColorAssembly to transform
  });
}

Dynamic Theme Generator

// app/theme/page.tsx
'use client';
 
import { useState } from 'react';
import { useColorAssembly } from '@/providers/ColorAssemblyProvider';
 
export default function ThemeGeneratorPage() {
  const api = useColorAssembly();
  const [baseColor, setBaseColor] = useState('#6366f1');
 
  const rgb = api.transforms.hexToRgb({ hex: baseColor });
  const palette = api.generate.semanticPalette(rgb, 'light');
  const scale = api.generate.primaryScale(rgb);
 
  return (
    <div className="container mx-auto p-8">
      <h1 className="text-3xl font-bold mb-8">Theme Generator</h1>
      
      <div className="mb-8">
        <label className="block mb-2 font-semibold">Base Color</label>
        <input
          type="color"
          value={baseColor}
          onChange={(e) => setBaseColor(e.target.value)}
          className="w-full h-16 rounded-lg cursor-pointer"
        />
      </div>
 
      <div className="grid md:grid-cols-2 gap-8">
        {/* Semantic Palette */}
        <div>
          <h2 className="text-xl font-semibold mb-4">Semantic Colors</h2>
          <div className="space-y-2">
            {Object.entries(palette).map(([name, rgb]) => {
              const hex = api.transforms.rgbToHex(rgb);
              return (
                <div key={name} className="flex items-center gap-4">
                  <div
                    className="w-16 h-16 rounded-lg border"
                    style={{ backgroundColor: hex.hex }}
                  />
                  <div>
                    <div className="font-semibold capitalize">{name}</div>
                    <div className="text-sm text-gray-600">{hex.hex}</div>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
 
        {/* Color Scale */}
        <div>
          <h2 className="text-xl font-semibold mb-4">Primary Scale</h2>
          <div className="grid grid-cols-5 gap-2">
            {scale.map((color, i) => {
              const hex = api.transforms.rgbToHex(color);
              return (
                <div key={i} className="text-center">
                  <div
                    className="w-full h-16 rounded-lg mb-2"
                    style={{ backgroundColor: hex.hex }}
                  />
                  <div className="text-xs text-gray-600">{(i + 1) * 100}</div>
                </div>
              );
            })}
          </div>
        </div>
      </div>
 
      {/* Export Button */}
      <button
        onClick={() => {
          const css = Object.entries(palette).map(([name, rgb]) => {
            const hex = api.transforms.rgbToHex(rgb);
            return `  --color-${name}: ${hex.hex};`;
          }).join('\n');
 
          const fullCss = `:root {\n${css}\n}`;
          navigator.clipboard.writeText(fullCss);
          alert('CSS variables copied to clipboard!');
        }}
        className="mt-8 px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
      >
        Export as CSS Variables
      </button>
    </div>
  );
}

Real-time Accessibility Dashboard

// app/accessibility/page.tsx
'use client';
 
import { useState } from 'react';
import { useColorAssembly } from '@/providers/ColorAssemblyProvider';
 
export default function AccessibilityDashboard() {
  const api = useColorAssembly();
  const [fgColor, setFgColor] = useState('#000000');
  const [bgColor, setBgColor] = useState('#ffffff');
 
  const fg = api.transforms.hexToRgb({ hex: fgColor });
  const bg = api.transforms.hexToRgb({ hex: bgColor });
  
  const wcag = api.analyze.checkContrast(fg, bg);
  const apca = api.analyze.checkApca(fg, bg);
  
  return (
    <div className="container mx-auto p-8">
      <h1 className="text-3xl font-bold mb-8">Accessibility Dashboard</h1>
 
      <div className="grid md:grid-cols-2 gap-8 mb-8">
        <div>
          <label className="block mb-2 font-semibold">Foreground (Text)</label>
          <input
            type="color"
            value={fgColor}
            onChange={(e) => setFgColor(e.target.value)}
            className="w-full h-16 rounded-lg"
          />
        </div>
        <div>
          <label className="block mb-2 font-semibold">Background</label>
          <input
            type="color"
            value={bgColor}
            onChange={(e) => setBgColor(e.target.value)}
            className="w-full h-16 rounded-lg"
          />
        </div>
      </div>
 
      {/* Preview */}
      <div
        className="p-8 rounded-lg mb-8"
        style={{ backgroundColor: bgColor, color: fgColor }}
      >
        <h2 className="text-2xl font-bold mb-4">Sample Heading</h2>
        <p className="mb-4">
          This is sample body text to preview the color combination.
          Make sure it's readable and meets accessibility standards.
        </p>
        <button className="px-4 py-2 border rounded">Button</button>
      </div>
 
      {/* WCAG Results */}
      <div className="grid md:grid-cols-2 gap-8">
        <div className="p-6 rounded-lg border">
          <h3 className="text-xl font-bold mb-4">WCAG 2.1</h3>
          <div className="text-3xl font-bold mb-4">
            {wcag.ratio.toFixed(2)}:1
          </div>
          <div className="space-y-2">
            <ResultBadge label="Normal Text AA" pass={wcag.normalTextAA} />
            <ResultBadge label="Normal Text AAA" pass={wcag.normalTextAAA} />
            <ResultBadge label="Large Text AA" pass={wcag.largeTextAA} />
            <ResultBadge label="Large Text AAA" pass={wcag.largeTextAAA} />
            <ResultBadge label="UI Components AA" pass={wcag.uiComponentsAA} />
          </div>
        </div>
 
        <div className="p-6 rounded-lg border">
          <h3 className="text-xl font-bold mb-4">APCA</h3>
          <div className="text-3xl font-bold mb-4">
            Lc {Math.abs(apca.lc).toFixed(1)}
          </div>
          <div className="space-y-2">
            <ResultBadge label="Body Text (75+)" pass={apca.meetsBodyText} />
            <ResultBadge label="Large Text (60+)" pass={apca.meetsLargeText} />
            <ResultBadge label="Non-text (45+)" pass={apca.meetsNonText} />
          </div>
          <div className="mt-4 text-sm text-gray-600">
            Min font size: {apca.fontSizeNormal}px (normal) / {apca.fontSizeBold}px (bold)
          </div>
        </div>
      </div>
    </div>
  );
}
 
function ResultBadge({ label, pass }: { label: string; pass: boolean }) {
  return (
    <div className={`flex items-center gap-2 text-sm ${pass ? 'text-green-600' : 'text-red-600'}`}>
      <span className="font-bold">{pass ? '✓' : '✗'}</span>
      <span>{label}</span>
    </div>
  );
}

Performance Optimization

Lazy Loading

// app/tools/page.tsx
import dynamic from 'next/dynamic';
 
const ColorPicker = dynamic(() => import('@/components/ColorPicker'), {
  ssr: false,
  loading: () => <div>Loading color picker...</div>,
});
 
export default function ToolsPage() {
  return <ColorPicker />;
}

Preload WASM

// app/layout.tsx
import { headers } from 'next/headers';
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <link
          rel="preload"
          href="/color-assembly.wasm"
          as="fetch"
          type="application/wasm"
          crossOrigin="anonymous"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

Next Steps

On this page