A designer hands you #FF5722, or an API sends "color": "#FF572280", and you need a Flutter Color. The literal is one line, but the wrong line compiles and gives you an invisible widget or a purple that should have been orange. This guide shows why Flutter puts the alpha byte first, gives a small parser for hex strings at runtime, and covers the current opacity APIs and the way back to a hex string.
Every example, printed value and error message was run with flutter test on Flutter 3.47.9 (Dart 3.13.7). The code passes flutter analyze with the default flutter_lints 6.0.0 rules. To convert a single colour by hand, paste it into the Flutter Color converter.
Quick lookup
| You have | Write |
|---|---|
#FF5722 from a design file | Color(0xFFFF5722) |
#FF572280 (CSS or Figma, 50% alpha) | Color(0x80FF5722) |
#F52 (short form) | Color(0xFFFF5522) |
| A hex string at runtime | colorFromHex(s), below |
| A colour at 50% opacity | color.withValues(alpha: 0.5) |
| A hex string from a Color | colorToCssHex(color), below |
Color(0xAARRGGBB): why alpha comes first
Color(int) takes one 32-bit number. The top byte is alpha, then red, green and blue, the same order Android uses. CSS and Figma write the colour channels first and put alpha last, or leave it out. So a hex code from a design tool needs 0xFF (fully opaque) in front of it:
const orange = Color(0xFFFF5722); // #FF5722
const halfOrange = Color(0x80FF5722); // #FF572280 in CSSUpper or lower case makes no difference: Color(0xffff5722) is == to Color(0xFFFF5722). As it happens, #FF5722 is also the value of Material's Colors.deepOrange.
Color(0xFF5722) is fully transparent
If you copy only the six digits, the number is 0x00FF5722. The missing top byte is zero, which means alpha 0. It compiles and runs, but nothing appears on screen:
const missingAlpha = Color(0xFF5722);
debugPrint('$missingAlpha');
// Color(alpha: 0.0000, red: 1.0000, green: 0.3412, blue: 0.1333, colorSpace: ColorSpace.sRGB)
const orange = Color(0xFFFF5722);
debugPrint('$orange');
// Color(alpha: 1.0000, red: 1.0000, green: 0.3412, blue: 0.1333, colorSpace: ColorSpace.sRGB)The default Flutter lints catch this in a literal. flutter analyze reports:
info • Instances of 'Color' should be created using an 8-digit hexadecimal integer (such as '0xFFFFFFFF') • use_full_hex_values_for_flutter_colorsThe lint only sees literals. The same mistake made at runtime gets no warning: Color(int.parse('FF5722', radix: 16)) is also 0x00FF5722, fully transparent. That is the usual way the bug reaches an app, and the next section avoids it.
Parse a hex string at runtime
Colours from an API, a CMS or a config file arrive as text in several shapes: with or without #, three digits, six digits, or eight digits with alpha. This helper accepts all of them and returns null for anything else, so a bad value cannot crash a screen:
import 'dart:ui';
/// Parses "#F52", "#FF5722", "FF5722" or an 8-digit hex colour.
///
/// 8 digits are read in CSS/Figma order (#RRGGBBAA), unless [alphaFirst]
/// is true or the string starts with 0x: then they are read in
/// Flutter/Android order (AARRGGBB). Returns null for anything else.
Color? colorFromHex(String input, {bool alphaFirst = false}) {
var hex = input.trim();
if (hex.startsWith('#')) {
hex = hex.substring(1);
} else if (hex.startsWith('0x') || hex.startsWith('0X')) {
hex = hex.substring(2);
alphaFirst = true;
}
if (!RegExp(r'^[0-9a-fA-F]+$').hasMatch(hex)) return null;
// #RGB and #RGBA: each digit is doubled, so F52 is FF5522.
if (hex.length == 3 || hex.length == 4) {
hex = hex.split('').map((d) => '$d$d').join();
}
int byte(int i) => int.parse(hex.substring(i, i + 2), radix: 16);
switch (hex.length) {
case 6:
return Color.fromARGB(255, byte(0), byte(2), byte(4));
case 8 when alphaFirst:
return Color.fromARGB(byte(0), byte(2), byte(4), byte(6));
case 8:
return Color.fromARGB(byte(6), byte(0), byte(2), byte(4));
default:
return null;
}
}Reading each byte separately keeps the channel order explicit, so one function handles both 8-digit orders and a six-digit string always gets alpha 255. The regular expression runs before int.parse, so int.parse never sees a bad character. Results, shown as the colour's 0xAARRGGBB value:
colorFromHex('#FF5722'); // 0xFFFF5722
colorFromHex('FF5722'); // 0xFFFF5722
colorFromHex(' #ff5722 '); // 0xFFFF5722
colorFromHex('#F52'); // 0xFFFF5522
colorFromHex('#FF572280'); // 0x80FF5722 (CSS order)
colorFromHex('80FF5722', alphaFirst: true); // 0x80FF5722
colorFromHex('0x80FF5722'); // 0x80FF5722
colorFromHex('#FF5722G'); // null
colorFromHex('orange'); // null
colorFromHex(''); // nullUse ?? to choose a fallback, for example colorFromHex(value) ?? Colors.grey. CSS names such as orange are not hex, so they return null here.
What int.parse throws
The shortest helpers you will find call int.parse(hex, radix: 16) directly. That throws a FormatException on anything unexpected, including a leading # you forgot to strip:
int.parse('#FF5722', radix: 16);
// FormatException: Invalid radix-16 number (at character 1)
// #FF5722
// ^
int.parse('FF5722G', radix: 16);
// FormatException: Invalid radix-16 number (at character 1)
// FF5722G
// ^
int.parse('', radix: 16);
// FormatException: Invalid radix-16 number (at character 1)
// ^Note that the position says character 1 for FF5722G too, although the bad character is the seventh, so do not rely on it. Leaving out radix gives a different message, because the string is read as a decimal number:
int.parse('FF5722');
// FormatException: Invalid radix-10 number (at character 1)
// FF5722
// ^
int.parse('0xFFFF5722'); // 4294924066, 0x is accepted without radix
int.tryParse('0xFFFF5722', radix: 16); // null, but not with radix: 16If you prefer exceptions to null, catch FormatException around the parse. Either way, check the length: a six-digit string passed straight to Color is the transparent-colour bug from the previous section.
#RRGGBBAA vs 0xAARRGGBB
An 8-digit string is ambiguous. CSS and Figma put alpha last; Flutter and Android XML colour resources put it first. Pasting a CSS value into a Flutter literal does not fail, it gives a different colour:
// CSS #FF572280 is orange at 50% alpha.
const wrong = Color(0xFF572280); // alpha FF, red 57, green 22, blue 80
// Back in CSS order: #572280, an opaque purple.
const right = Color(0x80FF5722); // move the last two digits to the front| Source | Order | Orange at 50% |
|---|---|---|
| CSS, Figma | #RRGGBBAA | #FF572280 |
| Flutter, Android XML | 0xAARRGGBB, #AARRGGBB | Color(0x80FF5722) |
Ask whoever produces the value which order they mean, then pass alphaFirst to match. The Flutter Color converter reads # plus 8 digits as CSS order and 0x plus 8 digits as Flutter order, the same rule as the helper above, and shows both.
Opacity: withValues, not withOpacity
Since Flutter moved Color to floating-point channels, several old members are deprecated. On Flutter 3.47.9, flutter analyze reports them like this:
info • 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss. Try replacing the use of the deprecated member with the replacement • deprecated_member_use
info • 'value' is deprecated and shouldn't be used. Use component accessors like .r or .g, or toARGB32 for an explicit conversion. Try replacing the use of the deprecated member with the replacement • deprecated_member_use
info • 'alpha' is deprecated and shouldn't be used. Use (*.a * 255.0).round().clamp(0, 255). Try replacing the use of the deprecated member with the replacement • deprecated_member_use
info • 'red' is deprecated and shouldn't be used. Use (*.r * 255.0).round().clamp(0, 255). Try replacing the use of the deprecated member with the replacement • deprecated_member_use
info • 'opacity' is deprecated and shouldn't be used. Use .a. Try replacing the use of the deprecated member with the replacement • deprecated_member_usegreen and blue are deprecated the same way. The replacements, none of which produce a warning:
const orange = Color(0xFFFF5722);
orange.withValues(alpha: 0.5); // instead of orange.withOpacity(0.5)
orange.withAlpha(128); // alpha as 0–255, still current
orange.toARGB32(); // instead of orange.value
(orange.a * 255).round(); // instead of orange.alpha
orange.g; // 0.3411764705882353, now 0.0–1.0
(orange.g * 255).round(); // 87, instead of orange.green
Color.fromARGB(128, 255, 87, 34); // still current
Color.fromRGBO(255, 87, 34, 0.5); // still current, 0x80FF5722An opacity becomes 8-bit alpha by rounding opacity × 255: withValues(alpha: 0.5) gives 0x80FF5722 and withValues(alpha: 0.1) gives 0x1AFF5722.
One catch when you compare colours: withValues keeps the exact 0.5, while a literal stores 128/255. They are not ==, even though they draw the same pixels:
orange.withValues(alpha: 0.5) == const Color(0x80FF5722); // false
orange.withAlpha(128) == const Color(0x80FF5722); // true
orange.withValues(alpha: 0.5).toARGB32() ==
const Color(0x80FF5722).toARGB32(); // trueIn tests and in == checks, compare toARGB32() values. The same applies to Material colours: Colors.deepOrange is a MaterialColor, so colorFromHex('#FF5722') == Colors.deepOrange is false, but their toARGB32() values are equal.
Color back to a hex string
toARGB32() returns the colour as an int in Flutter order. Split it into bytes to write CSS order, and pad each one, because toRadixString(16) drops leading zeros and writes lower case:
String _hex2(int v) => v.toRadixString(16).padLeft(2, '0').toUpperCase();
/// "#RRGGBB" for opaque colours, "#RRGGBBAA" otherwise (CSS order).
String colorToCssHex(Color color) {
final argb = color.toARGB32();
final a = argb >> 24 & 0xFF;
final rgb = '#${_hex2(argb >> 16 & 0xFF)}${_hex2(argb >> 8 & 0xFF)}'
'${_hex2(argb & 0xFF)}';
return a == 0xFF ? rgb : '$rgb${_hex2(a)}';
}
/// "0xAARRGGBB", ready to paste into Color(...).
String colorToDartLiteral(Color color) =>
'0x${color.toARGB32().toRadixString(16).padLeft(8, '0').toUpperCase()}';colorToCssHex(const Color(0xFFFF5722)); // #FF5722
colorToCssHex(const Color(0x80FF5722)); // #FF572280
colorToCssHex(colorFromHex('#F52')!); // #FF5522
colorToDartLiteral(const Color(0x0000FF00)); // 0x0000FF00
const Color(0xFFFF5722).toARGB32().toRadixString(16); // ffff5722Hex colours in a JSON model
When an API sends colours as strings, parse them in fromJson and write them back in the same format in toJson:
class Tag {
final String name;
final Color color;
const Tag({required this.name, required this.color});
factory Tag.fromJson(Map<String, dynamic> json) => Tag(
name: json['name'] as String,
color: colorFromHex(json['color'] as String) ?? Colors.grey,
);
Map<String, dynamic> toJson() => {
'name': name,
'color': colorToCssHex(color),
};
}With {"name": "urgent", "color": "#FF5722"}, color is 0xFFFF5722 and toJson sends "#FF5722" back. A value such as "red" falls back to grey. A missing color key fails earlier, at the cast, with type 'Null' is not a subtype of type 'String' in type cast; the Flutter JSON type errors guide explains that error. The JSON to Dart generator leaves colour fields as String, because it cannot tell a colour from other text; change them to Color with the helpers on this page.
Shadows are where alpha matters most, since almost every shadow colour is partly transparent. The BoxShadow generator writes shadow colours as full 8-digit Color(0xAARRGGBB) literals.