javascript
58 lines · 9 steps
Building a two-way color picker in JS
Convert between hex and RGB, then wire both representations to stay in sync as the user edits either one.
Explained by
highlit
1function hexToRgb(hex) {
2 const normalized = hex.replace(/^#/, '');
3 const full = normalized.length === 3
4 ? normalized.split('').map((c) => c + c).join('')
5 : normalized;
6
7 if (!/^[0-9a-f]{6}$/i.test(full)) {
8 throw new Error(`Invalid hex color: ${hex}`);
9 }
10
11 const int = parseInt(full, 16);
12 return {
13 r: (int >> 16) & 255,
14 g: (int >> 8) & 255,
15 b: int & 255,
16 };
17}
18
19function rgbToHex({ r, g, b }) {
20 const clamp = (n) => Math.max(0, Math.min(255, Math.round(n)));
21 const toHex = (n) => clamp(n).toString(16).padStart(2, '0');
22 return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
23}
24
25function initColorPicker(root) {
26 const swatch = root.querySelector('[data-swatch]');
27 const hexInput = root.querySelector('[data-hex]');
28 const channels = ['r', 'g', 'b'].map((key) => ({
29 key,
30 el: root.querySelector(`[data-channel="${key}"]`),
31 }));
32
33 const render = (rgb) => {
34 swatch.style.backgroundColor = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
35 hexInput.value = rgbToHex(rgb);
36 channels.forEach(({ key, el }) => { el.value = rgb[key]; });
37 };
38
39 channels.forEach(({ el }) => {
40 el.addEventListener('input', () => {
41 const rgb = channels.reduce(
42 (acc, { key, el }) => ({ ...acc, [key]: Number(el.value) }),
43 {},
44 );
45 render(rgb);
46 });
47 });
48
49 hexInput.addEventListener('change', () => {
50 try {
51 render(hexToRgb(hexInput.value));
52 } catch {
53 hexInput.classList.add('invalid');
54 }
55 });
56
57 render(hexToRgb(hexInput.value || '#3b82f6'));
58}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Packing RGB into a single integer lets bit shifts and masks pull each channel back out cleanly.
- 2A single render function fed by conversions keeps multiple UI representations of one value in sync.
- 3Validating and clamping at the conversion boundary keeps malformed input from corrupting the shared state.
Related explainers
typescript
import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { ValidationPipe } from '@nestjs/common'; import { ApiProperty } from '@nestjs/swagger';
Wiring validation and Swagger docs in NestJS
validation
openapi
decorators
Intermediate
8 steps
javascript
const ROLE_PERMISSIONS = { admin: ['users:read', 'users:write', 'billing:read', 'billing:write'], manager: ['users:read', 'billing:read'], member: ['users:read'],
Role-based permissions middleware in Express
authorization
middleware
rbac
Intermediate
9 steps
javascript
function attachThousandSeparators(input, { locale = 'en-US' } = {}) { const formatter = new Intl.NumberFormat(locale); const groupSep = formatter.format(11111).replace(/\d/g, '')[0] || ','; const decimalSep = formatter.format(1.1).replace(/\d/g, '')[0] || '.';
Live thousand separators without losing the caret
dom
intl
caret-preservation
Advanced
8 steps
javascript
import { useReducer, useEffect } from "react"; const initialState = { status: "idle", data: null, error: null };
Building a data-fetching hook in React
custom-hooks
usereducer
data-fetching
Intermediate
9 steps
javascript
const express = require('express'); const app = express(); app.get('/health', (req, res) => res.json({ status: 'ok' }));
Graceful shutdown in an Express server
graceful-shutdown
signal-handling
connection-tracking
Advanced
9 steps
typescript
import { parsePhoneNumberFromString, CountryCode } from 'libphonenumber-js'; export interface NormalizedPhone { e164: string;
Normalizing phone numbers to E.164 in TypeScript
validation
normalization
error-handling
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-two-way-color-picker-in-js-explained-javascript-a738/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.