Short answer
Because each platform defaults to different colour handling. The web treats hex and rgb() as sRGB and needs color(display-p3 …) or oklch() for wider colours; iOS apps are colour-managed and commonly use Display P3 assets; Android apps render sRGB unless an activity opts into wide-gamut mode. A token pipeline should store each colour with its colour space, emit the right form for each platform with an sRGB fallback, and test on real devices.
A design-system colour usually starts life as a hex value in a design tool and ends up in CSS, Swift and Kotlin. Each platform interprets numbers differently. In CSS, hex, rgb() and hsl() are sRGB by definition, and CSS Color Level 4 adds color(display-p3 …), lab() and oklch() for colours outside it. Apple platforms are colour-managed and Apple's guidelines encourage Display P3 colour and image assets with sRGB fallbacks. Android added colour management for wide colour spaces in Android 8.0; the default remains sRGB, and an activity must opt into wide-gamut mode — which Android warns costs memory and GPU work, and is granted only on displays that are both wide-gamut and colour-managed. The same '#FF3B30' can therefore be displayed as strict sRGB on one platform and stretched on another if somewhere in the chain a value is treated as the wrong space.
Most cross-platform differences a team notices are not panel differences but pipeline mistakes: a P3 value exported from a design tool as if it were sRGB, so it looks duller on the web; an sRGB value placed in an iOS asset marked as Display P3, so it looks more saturated on iPhone; an Android view that renders an image in its embedded P3 profile while the surrounding sRGB interface does not match it; or opacity blending that differs because one platform composites in linear light and another in encoded sRGB. The fix is the same in each case: know which space every value is in, and convert explicitly at export rather than letting defaults decide.
The Design Tokens Community Group's Color Module records a colour as a colour-space name plus components, with an optional hex fallback, and supports display-p3, oklch and a dozen other spaces. A pipeline built on that can emit, from one token, oklch() with an sRGB fallback for CSS, a Display P3 colour for Apple platforms, and an sRGB or wide-gamut value for Android depending on whether the app opts in. Where the wide-gamut value lies outside sRGB, the fallback should be a deliberate gamut mapping — reducing chroma at constant lightness and hue — rather than clipping each channel, which shifts hue and lightness. Check contrast on the fallback, since that is what most users on sRGB screens will see.
Even with a perfect pipeline, screens differ in brightness, white point, panel technology and user settings such as night shift, so no system can promise the same colour everywhere. It can promise the same intended colour: the same values in the same space, converted correctly for each platform. For UI colour that is usually enough, because users judge an interface on one device at a time. Where exact appearance matters — brand approvals, product photography — review on calibrated devices and document which device and mode the approval was made on. The digital-colour pillar covers why the same values still look different across screens; this page's concern is keeping the values themselves consistent.
| Platform | Default space for plain values | How to use wider colours | Watch out for |
|---|---|---|---|
| Web (CSS) | sRGB (hex, rgb(), hsl()) | color(display-p3 …), oklch(), lab() with sRGB fallback; @media (color-gamut: p3) | Fallback must be gamut-mapped and contrast-checked |
| Apple platforms | Colour-managed; asset catalogue records each colour's space | Display P3 colour sets and images with sRGB variants | sRGB values mislabelled as P3 look oversaturated |
| Android | sRGB | Opt in per activity with colorMode="wideColorGamut" (Android 8.0+) | Memory and GPU cost; only on wide-gamut, colour-managed displays |
| Token source | Whatever the file declares | DTCG Color Module: colorSpace + components + hex fallback | Tools that ignore colorSpace and read only hex |
Why: An sRGB value was stored in an asset marked Display P3.
Fix: Record each value's space in the token source and convert at export.
Why: The design was made in P3 and exported as hex without conversion.
Fix: Emit color(display-p3 …) or oklch() with a mapped sRGB fallback.
Why: Wide-gamut mode was enabled app-wide.
Fix: Enable it only on activities that show wide-gamut imagery.
Each statement is labelled by kind — established fact, a standard’s requirement, observed market data, a convention, or Colourwise’s own interpretation or analysis — with the strength of the evidence behind it.
FactStrong evidence
Android 8.0 (API level 26) introduced colour management for wide colour spaces; sRGB remains the default, apps opt in per activity with colorMode="wideColorGamut", wide-gamut mode uses more memory and GPU processing, and it is granted only on displays that are wide-gamut and colour-managed.
StandardStrong evidence
CSS treats hex, rgb() and hsl() colours as sRGB, and CSS Color Level 4 adds color(display-p3 …), lab() and oklch() to specify colours outside sRGB.
Source: CSS Color Module Level 4
ConventionModerate evidence
Apple's guidelines recommend Display P3 colour and image assets with sRGB fallbacks, and advise against hard-coding system colour values.
ConventionModerate evidence
The Design Tokens Color Module stores a colour with an explicit colour space, components and optional hex fallback, which lets one token be exported correctly for several platforms.
Reviewed 29 September 2026. Colourwise summarises its sources in its own words and does not reproduce standards text or proprietary colour data. Spotted an error? Tell us.