Vibe coding React Color Picker using Google Antigravity

Published: (February 24, 2026 at 03:27 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

Overview

I vibe‑coded an MVP version of a Color Picker this weekend for my ReactJS/NextJS projects using Google Antigravity. The goal was less a self‑test of JavaScript and more a challenge to see if Prompt Engineering could produce a component that I regularly use in my projects.

Technical Stack

  • Lit – for building the web component
  • Open Web Components – generator as a starting point
  • Rollup – module bundler (configured for both ESM and UMD outputs)
  • Chrome EyeDropper API – native eye‑dropper support
  • ElementInternals – form compatibility
  • Canvas‑based fallback – for browsers like Firefox that lack the EyeDropper API

All CSS is self‑contained and encapsulated within the component’s Shadow DOM; there are no external .css files or heavy dependencies.

Features

  • HEX and RGBA output formats
  • Shadow DOM encapsulation for style isolation
  • Canvas fallback eye‑dropper for Firefox
  • Multiple layouts to improve accessibility
  • Works natively in jQuery, ReactJS, NuxtJS, and NextJS

Requirements & Guardrails

Universal Compatibility

  • The library must work without external dependencies and be compiler‑independent (no Vite, Babel, Turbopack, etc.).
  • Build outputs: an ESM module and a UMD bundle via Rollup.

API & Events

  • Expose a standard CustomEvent named color-change that bubbles and is composed to cross the Shadow DOM boundary.
  • For React, provide a small wrapper or use a ref to ensure seamless event binding in React 18/19.

Server‑Side Rendering (SSR) Safety

  • Guard all browser‑specific APIs (window, document, customElements, etc.) with checks or defer execution until the component is mounted.
  • The library must be importable in a Node.js environment without throwing ReferenceError: window is not defined.

Form Integration

  • When placed inside a “, the component should behave like a native form control using the ElementInternals API.

Performance

  • Avoid heavy external color‑math libraries; implement lightweight internal utilities.
  • Strictly use Shadow DOM for style encapsulation.

Implementation Details

Rollup Configuration (rollup.config.js)

import { nodeResolve } from '@rollup/plugin-node-resolve';
import { terser } from 'rollup-plugin-terser';
import copy from 'rollup-plugin-copy';

export default {
  input: 'src/color-picker.js',
  output: [
    {
      file: 'dist/color-picker.esm.js',
      format: 'esm',
      sourcemap: true,
    },
    {
      file: 'dist/color-picker.umd.js',
      format: 'umd',
      name: 'ColorPicker',
      sourcemap: true,
    },
  ],
  plugins: [
    nodeResolve(),
    terser(),
    copy({
      targets: [{ src: 'src/*.css', dest: 'dist' }],
    }),
  ],
};

Core Component (color-picker.js)

import { LitElement, html, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';

@customElement('color-picker')
class ColorPicker extends LitElement {
  static styles = css`
    :host { display: block; }
    /* component‑specific styles go here */
  `;

  @property({ type: String }) value = '#000000';
  @state() _eyeDropperSupported = false;

  constructor() {
    super();
    if (typeof window !== 'undefined' && 'EyeDropper' in window) {
      this._eyeDropperSupported = true;
    }
  }

  connectedCallback() {
    super.connectedCallback();
    // Ensure any browser‑only APIs run after mount
  }

  _dispatchColorChange(color) {
    this.dispatchEvent(new CustomEvent('color-change', {
      detail: { color },
      bubbles: true,
      composed: true,
    }));
  }

  async _activateEyeDropper() {
    if (!this._eyeDropperSupported) return;
    try {
      const eyeDropper = new EyeDropper();
      const { sRGBHex } = await eyeDropper.open();
      this.value = `#${sRGBHex}`;
      this._dispatchColorChange(this.value);
    } catch (e) {
      console.error('EyeDropper cancelled or failed', e);
    }
  }

  render() {
    return html`
      
         {
                 this.value = e.target.value;
                 this._dispatchColorChange(this.value);
               }}>
        ${this._eyeDropperSupported
          ? html\`Eye Dropper\`
          : html\`\`}
        
      
    `;
  }
}

React Wrapper (ColorPickerReact.jsx)

import React, { useRef, useEffect } from 'react';
import '@my-org/color-picker'; // registers 

export default function ColorPickerReact({ onChange, ...props }) {
  const ref = useRef();

  useEffect(() => {
    const el = ref.current;
    const handler = e => onChange?.(e.detail.color);
    el?.addEventListener('color-change', handler);
    return () => el?.removeEventListener('color-change', handler);
  }, [onChange]);

  return ;
}

Web Test Runner Example

import { fixture, expect, a11yCheck } from '@open-wc/testing-helpers';
import './color-picker.js';

describe('color-picker', () => {
  it('passes accessibility audit', async () => {
    const el = await fixture('');
    await expect(el).to.be.accessible();
  });

  it('dispatches color-change on input', async () => {
    const el = await fixture('');
    const input = el.shadowRoot.querySelector('input');
    const spy = sinon.spy();
    el.addEventListener('color-change', spy);
    input.value = '#ff0000';
    input.dispatchEvent(new Event('input'));
    expect(spy).to.have.been.calledOnce;
    expect(spy.args[0][0].detail.color).to.equal('#ff0000');
  });
});

Future Steps

  • Refactor the code to be more human‑friendly and improve readability.
  • Dive deeper into the math behind color conversion to make the library scalable and maintainable.
  • Polish the UI/UX based on feedback and ensure the gradient generation meets quality expectations.
0 views
Back to Blog

Related posts

Read more »