Vibe coding React Color Picker using Google Antigravity
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
CustomEventnamedcolor-changethat bubbles and is composed to cross the Shadow DOM boundary. - For React, provide a small wrapper or use a
refto 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.