typescript
45 lines · 9 steps
Lightening and darkening hex colors in TypeScript
A small color toolkit parses hex strings into RGB, shifts each channel, and formats the result back to hex.
Explained by
highlit
1type RGB = { r: number; g: number; b: number };
2
3function parseHex(hex: string): RGB {
4 const normalized = hex.replace(/^#/, "").trim();
5 const expanded =
6 normalized.length === 3
7 ? normalized.split("").map((c) => c + c).join("")
8 : normalized;
9
10 if (!/^[0-9a-fA-F]{6}$/.test(expanded)) {
11 throw new Error(`Invalid hex color: ${hex}`);
12 }
13
14 const value = parseInt(expanded, 16);
15 return {
16 r: (value >> 16) & 0xff,
17 g: (value >> 8) & 0xff,
18 b: value & 0xff,
19 };
20}
21
22function toHex({ r, g, b }: RGB): string {
23 const channel = (n: number) =>
24 Math.round(clamp(n, 0, 255)).toString(16).padStart(2, "0");
25 return `#${channel(r)}${channel(g)}${channel(b)}`;
26}
27
28function clamp(n: number, min: number, max: number): number {
29 return Math.min(max, Math.max(min, n));
30}
31
32function adjust(hex: string, amount: number): string {
33 const { r, g, b } = parseHex(hex);
34 const shift = (channel: number) =>
35 amount >= 0
36 ? channel + (255 - channel) * amount
37 : channel * (1 + amount);
38
39 return toHex({ r: shift(r), g: shift(g), b: shift(b) });
40}
41
42const lighten = (hex: string, ratio: number) => adjust(hex, Math.abs(ratio));
43const darken = (hex: string, ratio: number) => adjust(hex, -Math.abs(ratio));
44
45export { parseHex, toHex, lighten, darken };
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Bit-shifting and masking cleanly slice a packed integer into separate byte channels.
- 2Validating input with a regex before parsing turns malformed data into a clear error instead of silent garbage.
- 3A single core function can spawn readable specializations by fixing the sign of one argument.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
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/lightening-and-darkening-hex-colors-in-typescript-explained-typescript-6ed1/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.