Manipulasi Warna JavaScript: Library dan Teknik
Embed This Widget
Add the script tag and a data attribute to embed this widget.
Embed via iframe for maximum compatibility.
<iframe src="https://colorfyi.com/iframe/entity//" width="420" height="400" frameborder="0" style="border:0;border-radius:10px;max-width:100%" loading="lazy"></iframe>
Paste this URL in WordPress, Medium, or any oEmbed-compatible platform.
https://colorfyi.com/entity//
Add a dynamic SVG badge to your README or docs.
[](https://colorfyi.com/entity//)
Use the native HTML custom element.
Warna dalam aplikasi web bukan nilai statis yang Anda tempel dari file desain dan dibiarkan begitu saja. Warna menjadi lebih terang saat hover, lebih gelap saat ditekan, menyesuaikan diri untuk persyaratan kontras aksesibilitas, beranimate di antara status, dan beradaptasi dengan tema yang dipilih pengguna. Semua itu membutuhkan manipulasi warna secara programatik — kemampuan untuk mem-parse, mengubah, dan menghasilkan nilai warna dalam JavaScript saat runtime.
Panduan ini mencakup cara JavaScript menangani warna secara native, matematika di balik transformasi umum, perbandingan tiga library yang paling banyak digunakan, dan cara membangun utilitas warna minimal ketika Anda menginginkan nol dependensi.
Parsing Hex, RGB, dan HSL dalam JavaScript
JavaScript tidak memiliki tipe warna bawaan. Warna datang sebagai string — "#FF5733", "rgb(255, 87, 51)", "hsl(11, 100%, 60%)" — dan Anda harus mem-parse-nya sendiri sebelum melakukan perhitungan apa pun.
Parsing Kode Hex
Warna hex adalah pengkodean kompak dari tiga (atau empat, dengan alpha) bilangan bulat satu byte dalam basis 16. Memparsing-nya adalah masalah memotong string dan memanggil parseInt:
function parseHex(hex) {
// Normalisasi: hapus # dan perluas singkatan (#F53 → #FF5533)
const clean = hex.replace('#', '');
const full = clean.length === 3
? clean.split('').map(c => c + c).join('')
: clean;
return {
r: parseInt(full.slice(0, 2), 16),
g: parseInt(full.slice(2, 4), 16),
b: parseInt(full.slice(4, 6), 16),
};
}
parseHex('#FF5733'); // { r: 255, g: 87, b: 51 }
parseHex('#F53'); // { r: 255, g: 85, b: 51 }
Kebalikannya — bilangan bulat kembali ke string hex — menggunakan toString(16) dengan padding nol:
function toHex({ r, g, b }) {
return '#' + [r, g, b]
.map(v => Math.round(v).toString(16).padStart(2, '0'))
.join('')
.toUpperCase();
}
toHex({ r: 255, g: 87, b: 51 }); // '#FF5733'
Parsing String RGB
String RGB dari DOM atau CSS-in-JS sering datang sebagai "rgb(255, 87, 51)". Regex mengekstrak tiga nilai:
function parseRgb(str) {
const match = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (!match) throw new Error(`RGB tidak valid: ${str}`);
return {
r: parseInt(match[1]),
g: parseInt(match[2]),
b: parseInt(match[3]),
};
}
parseRgb('rgb(255, 87, 51)'); // { r: 255, g: 87, b: 51 }
parseRgb('rgba(255, 87, 51, 0.5)'); // { r: 255, g: 87, b: 51 }
Parsing String HSL
String HSL — "hsl(11, 100%, 60%)" — memerlukan ekstraksi nilai derajat dan persentase:
function parseHsl(str) {
const match = str.match(/hsla?\((\d+),\s*([\d.]+)%,\s*([\d.]+)%/);
if (!match) throw new Error(`HSL tidak valid: ${str}`);
return {
h: parseFloat(match[1]),
s: parseFloat(match[2]),
l: parseFloat(match[3]),
};
}
Konversi antara RGB dan HSL
Sebagian besar perhitungan warna beroperasi dalam RGB (untuk pencampuran) atau HSL/HSV (untuk penyesuaian intuitif). Mengonversi di antara keduanya adalah keterampilan pertama yang Anda butuhkan:
function rgbToHsl({ r, g, b }) {
const rn = r / 255, gn = g / 255, bn = b / 255;
const max = Math.max(rn, gn, bn);
const min = Math.min(rn, gn, bn);
const l = (max + min) / 2;
const d = max - min;
if (d === 0) return { h: 0, s: 0, l: Math.round(l * 100) };
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
let h;
switch (max) {
case rn: h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6; break;
case gn: h = ((bn - rn) / d + 2) / 6; break;
default: h = ((rn - gn) / d + 4) / 6;
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100),
};
}
rgbToHsl({ r: 255, g: 87, b: 51 }); // { h: 11, s: 100, l: 60 }
Matematika Warna: Lighten, Darken, Desaturate
Setelah Anda memiliki warna dalam HSL, menyesuaikannya adalah aritmetika. Tiga sumbu HSL langsung memetakan ke penyesuaian yang paling sering diminta desainer.
Mencerahkan dan Menggelapkan
Meningkatkan atau menurunkan saluran l (kecerahan) adalah pendekatan yang langsung:
function lighten(hex, amount) {
const hsl = rgbToHsl(parseHex(hex));
hsl.l = Math.min(100, hsl.l + amount);
return hslToHex(hsl);
}
function darken(hex, amount) {
const hsl = rgbToHsl(parseHex(hex));
hsl.l = Math.max(0, hsl.l - amount);
return hslToHex(hsl);
}
lighten('#FF5733', 15); // Coral yang lebih terang
darken('#FF5733', 15); // Merah-oranye yang lebih gelap
Ini bekerja dengan baik untuk penyesuaian kecil. Untuk menghasilkan skala 50–950 penuh (seperti yang dilakukan Tailwind CSS), matematikanya lebih kompleks karena kecerahan yang dirasakan tidak linier dalam HSL — Generator Bayangan menangani hal ini dengan distribusi berbobot secara perseptual.
Desaturasi
Menurunkan saluran s (saturasi) ke arah 0 mengubah warna apa pun menjadi abu-abu:
function desaturate(hex, amount) {
const hsl = rgbToHsl(parseHex(hex));
hsl.s = Math.max(0, hsl.s - amount);
return hslToHex(hsl);
}
function grayscale(hex) {
return desaturate(hex, 100);
}
desaturate('#FF5733', 50); // Oranye dengan saturasi rendah yang teredam
grayscale('#FF5733'); // Abu-abu murni dengan kecerahan yang sama
Mencampur Dua Warna
Interpolasi linier dalam ruang RGB adalah perpaduan yang paling sederhana:
function mix(hex1, hex2, weight = 0.5) {
const c1 = parseHex(hex1);
const c2 = parseHex(hex2);
return toHex({
r: Math.round(c1.r * weight + c2.r * (1 - weight)),
g: Math.round(c1.g * weight + c2.g * (1 - weight)),
b: Math.round(c1.b * weight + c2.b * (1 - weight)),
});
}
mix('#FF5733', '#FFFFFF', 0.7); // 70% coral, 30% putih → sebuah tint
mix('#FF5733', '#000000', 0.7); // 70% coral, 30% hitam → sebuah shade
Pencampuran RGB menghasilkan hasil yang terasa datar secara perseptual di antara warna-warna komplementer. Untuk titik tengah yang lebih baik, library seperti chroma.js menawarkan pencampuran dalam ruang OKLCH atau Lab.
Rasio Kontras (WCAG)
Rasio kontras WCAG dihitung dari luminans relatif, bukan nilai saluran mentah. Luminans memerlukan koreksi gamma — kebalikan dari pengkodean yang diterapkan saat nilai sRGB disimpan:
function relativeLuminance({ r, g, b }) {
const linearize = (v) => {
const sRGB = v / 255;
return sRGB <= 0.04045
? sRGB / 12.92
: Math.pow((sRGB + 0.055) / 1.055, 2.4);
};
const R = linearize(r), G = linearize(g), B = linearize(b);
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
}
function contrastRatio(hex1, hex2) {
const L1 = relativeLuminance(parseHex(hex1));
const L2 = relativeLuminance(parseHex(hex2));
const lighter = Math.max(L1, L2);
const darker = Math.min(L1, L2);
return (lighter + 0.05) / (darker + 0.05);
}
contrastRatio('#FF5733', '#FFFFFF'); // ~3.0 — gagal WCAG AA untuk teks normal
contrastRatio('#000000', '#FFFFFF'); // 21.0 — kontras maksimum
WCAG AA memerlukan 4,5:1 untuk teks normal dan 3:1 untuk teks besar. Periksa pasangan apa pun dengan Konverter Warna.
Perbandingan Library Populer: chroma.js, culori, tinycolor2
Untuk apa pun selain penyesuaian sederhana, library khusus menghemat waktu yang signifikan dan menghindari bug kasus tepi dalam perhitungan warna.
chroma.js
Ukuran: ~13KB terkompresi | Kematangan: 2013, dirawat aktif | Lisensi: BSD
chroma.js adalah library warna JavaScript yang paling banyak dikenal. API-nya lancar dan dapat dirantai:
import chroma from 'chroma-js';
// Parse format apa pun
const color = chroma('#FF5733');
// Penyesuaian
color.darken(1).hex(); // '#D93B10'
color.lighten(1).hex(); // '#FF8066'
color.saturate(0.5).hex(); // '#FF4719'
color.desaturate(0.5).hex();// '#F2653F'
// Konversi
color.rgb(); // [255, 87, 51]
color.hsl(); // [11, 1, 0.6]
color.lab(); // [52.8, 47.1, 44.7]
color.oklch();// [0.63, 0.19, 27.5]
// Campur dalam ruang warna berbeda
chroma.mix('#FF5733', '#3B82F6', 0.5, 'rgb'); // Dalam RGB (titik tengah datar)
chroma.mix('#FF5733', '#3B82F6', 0.5, 'oklch'); // Dalam OKLCH (titik tengah semarak)
// Pembuatan skala
const scale = chroma.scale(['#FFF5F5', '#FF5733', '#7F0000'])
.mode('oklch')
.colors(9);
// Kontras
chroma.contrast('#FF5733', '#FFFFFF'); // 3.0
chroma.js adalah pilihan yang tepat ketika Anda membutuhkan library yang komprehensif dan terdokumentasi dengan baik dengan dukungan ruang warna yang baik termasuk pencampuran OKLCH.
culori
Ukuran: ~6KB terkompresi (dapat di-tree-shake) | Kematangan: 2019, dirawat aktif | Lisensi: MIT
culori adalah library modern dan fungsional yang dirancang untuk tree-shaking. Setiap operasi adalah fungsi yang berdiri sendiri — Anda hanya mengimpor apa yang Anda gunakan:
import { parse, formatHex, oklch, interpolate, formatCss } from 'culori';
// Parse
const color = parse('#FF5733'); // { mode: 'rgb', r: 1, g: 0.34, b: 0.2 }
// Konversi ke OKLCH
const inOklch = oklch(color); // { mode: 'oklch', l: 0.63, c: 0.19, h: 27.5 }
// Sesuaikan kecerahan
const lighter = { ...inOklch, l: inOklch.l + 0.1 };
formatHex(lighter); // '#FF8469'
// Interpolasi untuk gradien/animasi
const gradient = interpolate(['#FF5733', '#3B82F6'], 'oklch');
formatCss(gradient(0.5)); // Warna titik tengah dalam format CSS
// Pembuatan skala
import { samples } from 'culori';
const stops = samples(5).map(t => formatHex(gradient(t)));
// ['#FF5733', '#E85E56', '#9B6DE3', '#6186F0', '#3B82F6']
culori beroperasi pada objek polos dengan properti mode, yang membuatnya mudah untuk melakukan serialisasi warna, menyimpannya dalam state, atau mengirimnya melalui jaringan. Ini adalah pilihan terbaik untuk proyek TypeScript modern di mana ukuran bundle penting dan tree-shaking sedang digunakan.
tinycolor2
Ukuran: ~5KB terkompresi | Kematangan: 2012, stabil (kurang aktif) | Lisensi: MIT
tinycolor2 adalah parser terkecil dan paling permisif — ia menerima hampir semua format string warna, termasuk warna bernama CSS, dan bekerja tanpa konfigurasi:
import tinycolor from 'tinycolor2';
// Parse hampir apa saja
tinycolor('red').toHexString(); // '#FF0000'
tinycolor('hsl(11, 100%, 60%)').toHexString(); // '#FF5733'
tinycolor('#F53').toHexString(); // '#FF5533'
// Penyesuaian
tinycolor('#FF5733').lighten(15).toHexString(); // '#FF8966'
tinycolor('#FF5733').darken(15).toHexString(); // '#C92B04'
tinycolor('#FF5733').desaturate(30).toHexString(); // '#EB6643'
tinycolor('#FF5733').spin(180).toHexString(); // '#33AEff' (komplemen)
// Keterbacaan / kontras
tinycolor.readability('#FF5733', '#FFFFFF'); // 3.0
tinycolor.isReadable('#FF5733', '#FFFFFF'); // false (gagal WCAG AA)
tinycolor.isReadable('#FF5733', '#FFFFFF', { level: 'AA', size: 'large' }); // true
// Harmoni warna
tinycolor('#FF5733').triad(); // [TinyColor, TinyColor, TinyColor]
tinycolor('#FF5733').analogous(); // 6 warna analogus
tinycolor('#FF5733').complement(); // Satu warna komplementer
tinycolor2 adalah pilihan yang tepat untuk proyek yang membutuhkan parsing string warna yang dimasukkan pengguna yang dapat andal (yang mungkin dalam format apa pun) dan manipulasi dasar tanpa menarik dependensi yang lebih besar.
Ringkasan Perbandingan Library
| Fitur | chroma.js | culori | tinycolor2 |
|---|---|---|---|
| Ukuran bundle | ~13KB | ~6KB (dapat di-tree-shake) | ~5KB |
| Ruang warna | RGB, HSL, Lab, LCH, OKLCH | 20+ termasuk OKLCH, P3 | RGB, HSL, HSV |
| Gaya API | Lancar/berantai | Fungsional | Berorientasi objek |
| TypeScript | Tipe komunitas | Bawaan | Tipe komunitas |
| Pencampuran warna | RGB, HSL, Lab, OKLCH | Ruang warna apa pun | RGB saja |
| Terbaik untuk | Penggunaan komprehensif, visualisasi data | Bundle TS modern | Parsing, operasi sederhana |
Membangun Utilitas Warna dari Awal
Untuk aplikasi produksi yang hanya membutuhkan parsing hex, mencerahkan, menggelapkan, dan pemeriksaan kontras, utilitas tanpa dependensi sering kali merupakan pilihan arsitektur yang lebih baik. Berikut adalah implementasi lengkapnya:
// color-utils.js
export function parseHex(hex) {
const clean = hex.replace('#', '');
const full = clean.length === 3
? clean.split('').map(c => c + c).join('')
: clean;
return {
r: parseInt(full.slice(0, 2), 16),
g: parseInt(full.slice(2, 4), 16),
b: parseInt(full.slice(4, 6), 16),
};
}
export function toHex({ r, g, b }) {
return '#' + [r, g, b]
.map(v => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0'))
.join('').toUpperCase();
}
function rgbToHsl({ r, g, b }) {
const rn = r / 255, gn = g / 255, bn = b / 255;
const max = Math.max(rn, gn, bn), min = Math.min(rn, gn, bn);
const l = (max + min) / 2;
const d = max - min;
if (d === 0) return { h: 0, s: 0, l };
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
let h;
switch (max) {
case rn: h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6; break;
case gn: h = ((bn - rn) / d + 2) / 6; break;
default: h = ((rn - gn) / d + 4) / 6;
}
return { h, s, l };
}
function hslToRgb({ h, s, l }) {
if (s === 0) {
const v = Math.round(l * 255);
return { r: v, g: v, b: v };
}
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
return {
r: Math.round(hue2rgb(p, q, h + 1/3) * 255),
g: Math.round(hue2rgb(p, q, h) * 255),
b: Math.round(hue2rgb(p, q, h - 1/3) * 255),
};
}
export function lighten(hex, amount) {
const hsl = rgbToHsl(parseHex(hex));
return toHex(hslToRgb({ ...hsl, l: Math.min(1, hsl.l + amount / 100) }));
}
export function darken(hex, amount) {
const hsl = rgbToHsl(parseHex(hex));
return toHex(hslToRgb({ ...hsl, l: Math.max(0, hsl.l - amount / 100) }));
}
export function desaturate(hex, amount) {
const hsl = rgbToHsl(parseHex(hex));
return toHex(hslToRgb({ ...hsl, s: Math.max(0, hsl.s - amount / 100) }));
}
function linearize(v) {
const sRGB = v / 255;
return sRGB <= 0.04045 ? sRGB / 12.92 : Math.pow((sRGB + 0.055) / 1.055, 2.4);
}
export function relativeLuminance(hex) {
const { r, g, b } = parseHex(hex);
return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b);
}
export function contrastRatio(hex1, hex2) {
const L1 = relativeLuminance(hex1);
const L2 = relativeLuminance(hex2);
const lighter = Math.max(L1, L2);
const darker = Math.min(L1, L2);
return (lighter + 0.05) / (darker + 0.05);
}
export function isWcagAA(foreground, background, largeText = false) {
const ratio = contrastRatio(foreground, background);
return ratio >= (largeText ? 3.0 : 4.5);
}
export function bestTextColor(background) {
const L = relativeLuminance(background);
return L > 0.179 ? '#000000' : '#FFFFFF';
}
Penggunaan:
import { lighten, darken, contrastRatio, bestTextColor } from './color-utils.js';
const brand = '#FF5733';
const hover = darken(brand, 10); // Lebih gelap untuk :hover
const light = lighten(brand, 30); // Tint terang untuk latar belakang
const text = bestTextColor(brand); // Hitam atau putih untuk teks di atas merek
console.log(contrastRatio(text, brand)); // Harus ≥ 4.5
Pertimbangan Performa untuk Warna Runtime
Cache Secara Agresif
Perhitungan warna bersifat referensially transparent — input hex yang sama selalu menghasilkan output yang sama. Wrapper memoization sederhana menghindari perhitungan ulang pada setiap render:
function memoize(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (!cache.has(key)) cache.set(key, fn(...args));
return cache.get(key);
};
}
const lightenCached = memoize(lighten);
const contrastCached = memoize(contrastRatio);
CSS Custom Properties daripada JavaScript
Untuk theming dinamis, jangan hitung ulang warna dalam JavaScript pada setiap perubahan state. Hitung palet sekali, tulis ke custom properties CSS, dan biarkan CSS menangani setiap komponen:
function applyTheme(brandHex) {
const root = document.documentElement;
root.style.setProperty('--brand', brandHex);
root.style.setProperty('--brand-dark', darken(brandHex, 10));
root.style.setProperty('--brand-light', lighten(brandHex, 30));
root.style.setProperty('--on-brand', bestTextColor(brandHex));
}
// Dipanggil sekali saat warna merek berubah, tidak pada setiap render
applyTheme('#FF5733');
Hindari Parsing String dalam Loop Render
Mem-parsing "rgb(255, 87, 51)" di dalam fungsi render React yang berjalan 60 kali per detik adalah pemborosan. Parse sekali, simpan hasilnya, dan lewatkan objek warna terstruktur melalui pohon komponen Anda daripada string mentah.
// Mahal: mem-parse pada setiap panggilan
const color = chroma(colorString).darken(1).hex();
// Lebih baik: parse sekali, transformasi berkali-kali
const parsed = chroma(colorString);
const darkVariant = parsed.darken(1).hex();
const lightVariant = parsed.lighten(1).hex();
Untuk pembuatan warna runtime dalam skala besar — color picker yang diperbarui pada setiap gerakan mouse, atau visualisasi dengan ratusan warna yang dihitung — culori adalah pilihan terbaik karena arsitektur fungsional tanpa state-nya dan ukuran bundle yang kecil memiliki overhead paling sedikit per panggilan.
Poin-Poin Utama
- JavaScript tidak memiliki tipe warna native — parse string hex, RGB, dan HSL menjadi objek terstruktur sebelum melakukan perhitungan apa pun.
- Aritmetika HSL (menyesuaikan
luntuk kecerahan,suntuk saturasi) mencakup sebagian besar transformasi desain umum secara langsung. - Rasio kontras WCAG memerlukan perhitungan luminans dengan koreksi gamma — jangan perkirakan dengan matematika saluran kecerahan.
- chroma.js adalah library paling lengkap dengan dukungan ruang warna yang sangat baik termasuk pencampuran OKLCH; culori adalah pilihan modern yang dapat di-tree-shake untuk proyek TypeScript; tinycolor2 adalah parser paling permisif untuk skenario input pengguna.
- Utilitas tanpa dependensi layak untuk aplikasi yang hanya membutuhkan parsing, mencerahkan/menggelapkan, dan kontras — implementasi lengkapnya di bawah 100 baris.
- Cache warna yang dihitung secara agresif dan tulis hasil ke custom properties CSS daripada menghitung ulang per render.
- Gunakan Konverter Warna untuk memverifikasi transformasi warna apa pun secara visual, dan Generator Bayangan untuk menghasilkan skala 50–950 yang kompatibel dengan Tailwind dari satu hex merek.