Picking Hex Codes For Skin Tones Is A Lot Harder Than People Think

Most designers grab the first swatch from a picker that looks close and call it done. It usually doesn't look close once it's applied to a dark UI background or projected on a mobile screen at 60 percent opacity. I spent three years dealing with this problem for a design system that needed to cover 47 distinct skin tones across four different brand palettes. We ended up building a reference sheet that actually works instead of guessing. The way I approach this starts with a specific problem: skin color isn't a single value you can just type into a color picker and expect to match. It's a combination of hue, lightness, and saturation that varies dramatically depending on whether you're working in sRGB, Display P3, or Adobe RGB. Hex codes are locked to sRGB, which means every time you pick a shade you're already making a compromise. The trick is picking the right compromise. I use the Munsell color system as a starting point because it separates hue from value in a way that matches human perception better than HSL or HSV. When I'm building out a palette, I map my chosen Munsell values to hex through a conversion table I built years ago. It's not perfect. The conversion introduces a small error band, but it's small enough that you can refine by eye after the fact. The alternative is just guessing and ending up with colors that shift when they move between screens.

Here's the actual problem I ran into that forced me to change my entire workflow. I was working on a healthcare dashboard that used skin tone illustrations for medical diagrams. The designer picked a set of hex codes that looked correct on their calibrated monitor. When we pushed the build to staging, everyone in QA said the tones looked wrong. I pulled the files and realized the issue: the design system was exporting colors in sRGB profile, but the preview environment was using a different gamma curve. The tones weren't wrong. The rendering was wrong. I had to set the CSS to use explicit color space declarations like color: oklch(0.65 0.12 25); and switch the entire design token pipeline to oklch instead of hex for production. Hex is still fine for static assets, but if your product ships colors dynamically you need to move away from it early. If you're sticking with hex, here are the values I land on after going through my calibration process. These are sRGB approximations of real skin tones across the Fitzpatrick scale I and VI. Fitzpatrick I (very fair, typically northern European): #FCEDEA to #F5DDD6. This range covers the lightest end. Don't go lighter than #FCEDEA if you're trying to represent actual skin. Anything lighter reads as bone or paper, not flesh. I've seen too many systems default to #FFE0BD for their lightest tone, which is basically orange with a pink tint and looks nothing like human skin under normal lighting conditions.

Fitzpatrick II (fair): #F5C7A3 to #E8B08A. This is where a lot of design systems start going wrong because they jump from the very light range straight to mid-tones without accounting for the warm undertones in this band. If you pick a hex without considering warmth, it'll look sickly rather than natural. I add a slight red channel boost of about 3 to 5 percent when converting from my source color space to fix this. Fitzpatrick III (moderate light): #D49A6A to #C48B5C. This is the most common range in design systems that claim to be inclusive but actually aren't. Most palettes have exactly one color here, usually wrong, and assume that covers the middle. You need at least three distinct values in this range. The undertone variation between olive, neutral, and warm is significant enough that a single hex will miss a large portion of people who fall in this category. Fitzpatrick IV (moderate): #A6734B to #8D5E3C. The jump from Fitzpatrick III to IV is larger than the jump from I to II. If your palette spacing is linear, you'll undersample this transition. I space my hex values logarithmically across the lightness axis instead, which gives more breathing room in the darker ranges where perceptual differences are smaller but still important.

Get the Full Details

The Ultimate Guide to Skin Tone Color Palettes: Find Your Perfect Match
The Ultimate Guide to Skin Tone Color Palettes: Find Your Perfect Match

Fitzpatrick V (brown): #7A5235 to #634028. This range gets almost no representation in commercial design systems. When it does appear, it's often a desaturated brown that looks more like cardboard than skin. The key difference is saturation. Real skin in this range holds significant chroma. A hex like #8B6914 or #6F4E37 will read more accurately than anything below #5A3A25. Fitzpatrick VI (dark brown to black): #5C3A1E to #3A2312. The common mistake here is pushing the hex too dark. #2B1A0D and darker stops being skin color and becomes a shadow. The lightest value in this range, #5C3A1E, might look too warm to someone who's never worked with dark skin tones in digital design. That's normal. Under correct lighting conditions, this is accurate. If you're building for accessibility, test these values against your background colors at both 3:1 and 4.5:1 contrast ratios. Dark skin tones on dark backgrounds are a frequent failure point in WCAG audits. I should be honest about the limitations of hex for this purpose. Hex can't represent the full gamut of skin colors because it's locked to sRGB. When you're working on web projects that support wide color gamut displays, your hex values will look desaturated compared to what the skin actually is. If your product ships on Apple devices or newer Android phones, consider using oklab or oklch for the dynamic color tokens and only fall back to hex for static assets like SVGs and PNGs.

Another issue that catches people off guard is how different skin tones render on different display technologies. An OLED screen will make #8D5E3C look noticeably warmer and richer than an LCD panel. This isn't a design problem you can fully solve, but you can reduce its impact by avoiding colors that sit right on the boundary between two perceptually distinct ranges. If #A6734B and #A6734C look like different colors to you on your screen, someone else on a different monitor will see a bigger gap. Keep your values spaced by at least 3 to 5 points on the lightness axis to avoid this boundary problem. When I export these values for a team handoff, I include the hex code, the oklch equivalent, the approximate Munsell value, and the Fitzpatrick category it represents. Four data points per color. It takes more space in the documentation, but it eliminates about 80 percent of the back-and-forth that usually happens when developers or other designers question why a specific shade was chosen. They can see the conversion chain and verify it themselves instead of asking me to justify every selection. If you need a ready-made palette I use, the core set is small enough to paste directly into your CSS custom properties. I keep it to 12 values rather than 47 because 47 creates a false impression of coverage without actually giving you enough range to distinguish between similar tones. Twelve well-spaced values beat twenty poorly spaced ones every time.