C# implementation of Google's Material color utilities
$ dotnet add package MaterialColorUtilitiesC# implementation of Google's Material Color Utilities
Includes all of the algorithms you might need for adding Material You colors to your .NET app:
TonalPalette allows easy access to the tones of a color, a CorePalette contains the 6 key tonal palettes: Primary, Secondary, Tertiary, Neutral, NeutralVariant and ErrorPrimary, TertiaryContainer or OnErrorOther stuff:
Get the packages from Nuget:
| Package | Description | Link |
|---|---|---|
MaterialColorUtilites | Contains all of the color algorithms and helpers |
MaterialColorUtilites.Maui | Adds dynamic colors to your .NET MAUI app |
Adding beautiful Material You dynamic colors to your app is super simple, just follow these instructions. The library will handle everything else.
Light/dark mode handling works on every platform, but accent color is different on every platform:
StorageRead permission is granted, extract a color from the wallpaper, otherwise use the default seedMaterialColorUtilites.Maui package using the link aboveMauiProgram.cs+using MaterialColorUtilities.Maui;
namespace YourBeautifulApp;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
MauiAppBuilder builder = MauiApp
.CreateBuilder()
+ .UseMaterialDynamicColors()
.UseMauiApp<App>();
return builder.Build();
}
}App.xaml for suggestions when writing XAML<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:YourBeautifulApp"
x:Class="YourBeautifulApp.App">
<Application.Resources>
+ <Color x:Key="Primary" />
+ <Color x:Key="PrimaryContainer" />
+ <Color x:Key="Secondary" />
+ <Color x:Key="SecondaryContainer" />
+ <Color x:Key="Tertiary" />
+ <Color x:Key="TertiaryContainer" />
+ <Color x:Key="Surface" />
+ <Color x:Key="SurfaceVariant" />
+ <Color x:Key="Background" />
+ <Color x:Key="Error" />
+ <Color x:Key="ErrorContainer" />
+ <Color x:Key="OnPrimary" />
+ <Color x:Key="OnPrimaryContainer" />
+ <Color x:Key="OnSecondary" />
+ <Color x:Key="OnSecondaryContainer" />
+ <Color x:Key="OnTertiary" />
+ <Color x:Key="OnTertiaryContainer" />
+ <Color x:Key="OnSurface" />
+ <Color x:Key="OnSurfaceVariant" />
+ <Color x:Key="OnError" />
+ <Color x:Key="OnErrorContainer" />
+ <Color x:Key="OnBackground" />
+ <Color x:Key="Outline" />
+ <Color x:Key="Shadow" />
+ <Color x:Key="InverseSurface" />
+ <Color x:Key="InverseOnSurface" />
+ <Color x:Key="InversePrimary" />
+ <Color x:Key="Surface1" />
+ <Color x:Key="Surface2" />
+ <Color x:Key="Surface3" />
+ <Color x:Key="Surface4" />
+ <Color x:Key="Surface5" />
+ <SolidColorBrush x:Key="PrimaryBrush" />
+ <SolidColorBrush x:Key="PrimaryContainerBrush" />
+ <SolidColorBrush x:Key="SecondaryBrush" />
+ <SolidColorBrush x:Key="SecondaryContainerBrush" />
+ <SolidColorBrush x:Key="TertiaryBrush" />
+ <SolidColorBrush x:Key="TertiaryContainerBrush" />
+ <SolidColorBrush x:Key="SurfaceBrush" />
+ <SolidColorBrush x:Key="SurfaceVariantBrush" />
+ <SolidColorBrush x:Key="BackgroundBrush" />
+ <SolidColorBrush x:Key="ErrorBrush" />
+ <SolidColorBrush x:Key="ErrorContainerBrush" />
+ <SolidColorBrush x:Key="OnPrimaryBrush" />
+ <SolidColorBrush x:Key="OnPrimaryContainerBrush" />
+ <SolidColorBrush x:Key="OnSecondaryBrush" />
+ <SolidColorBrush x:Key="OnSecondaryContainerBrush" />
+ <SolidColorBrush x:Key="OnTertiaryBrush" />
+ <SolidColorBrush x:Key="OnTertiaryContainerBrush" />
+ <SolidColorBrush x:Key="OnSurfaceBrush" />
+ <SolidColorBrush x:Key="OnSurfaceVariantBrush" />
+ <SolidColorBrush x:Key="OnErrorBrush" />
+ <SolidColorBrush x:Key="OnErrorContainerBrush" />
+ <SolidColorBrush x:Key="OnBackgroundBrush" />
+ <SolidColorBrush x:Key="OutlineBrush" />
+ <SolidColorBrush x:Key="ShadowBrush" />
+ <SolidColorBrush x:Key="InverseSurfaceBrush" />
+ <SolidColorBrush x:Key="InverseOnSurfaceBrush" />
+ <SolidColorBrush x:Key="InversePrimaryBrush" />
+ <SolidColorBrush x:Key="Surface1Brush" />
+ <SolidColorBrush x:Key="Surface2Brush" />
+ <SolidColorBrush x:Key="Surface3Brush" />
+ <SolidColorBrush x:Key="Surface4Brush" />
+ <SolidColorBrush x:Key="Surface5Brush" />
</Application.Resources>
</Application>Specify a fallback seed color as an argument to the extension method or use a lambda for more options:
.UseMaterialDynamicColors(options =>
{
options.FallbackSeed = unchecked((int)0xFFB000B5);
options.UseDynamicColor = false;
})<Button Color="{DynamicResource Primary}" />Button button = new();
button.SetDynamicResource(Button.BackgroundColorProperty, MaterialColorUtilities.Schemes.Keys.Primary);DynamicColorService from the MAUI dependency injection container:using MaterialColorUtilites.Maui;
public class MyService
{
public MyService(DynamicColorService dynamicColorService)
{
Scheme<Color> scheme = dynamicColorService.SchemeMaui;
}
}
For samples check out the Playground.Maui project.
Using custom components (like CorePalette, schemes and mappers) is supported. Just subclass DynamicColorService and supply your own types as generic arguments.
public class MyDynamicColorService : DynamicColorService<MyCorePalette, MyScheme<int>, MyScheme<Color>, MyLightSchemeMapper, MyDarkSchemeMapper>
{
// You can also override Initialize() and Apply() for custom logic
}Then use it using the generic extension method:
.UseMaterialDynamicColors<MyDynamicColorService>()Google's new color space designed for meeting contrast standards easy. Properties:
Convert an RGB color to HCT, change its Tone, then convert back:
int argb = Colors.Azure.ToInt();
Hct hct = Hct.FromInt(argb);
hct.Tone = 30;
int tone30 = hct.ToInt();Check out the Blazor playground running live here or Material Theme Builder for visualisations.
Used for retrieving tones of a color.
// Does the same as the above code
TonalPalette palette = TonalPalette.FromInt(argb);
int tone30 = palette[30];Contains the Tonal palettes of the key colors.
Properties:
public TonalPalette Primary { get; set; }
public TonalPalette Secondary { get; set; }
public TonalPalette Tertiary { get; set; }
public TonalPalette Neutral { get; set; }
public TonalPalette NeutralVariant { get; set; }
public TonalPalette Error { get; set; }Check out the source code if you want to know how they get constructed.
Schemes store the Material Design 3 color role values.
public class Scheme<TColor>
{
public TColor Primary { get; set; }
public TColor OnPrimary { get; set; }
public TColor PrimaryContainer { get; set; }
public TColor OnPrimaryContainer { get; set; }
public TColor Secondary { get; set; }
public TColor OnSecondary { get; set; }
public TColor SecondaryContainer { get; set; }
...
}Schemes are generic so you can use whatever color model you want.
This library uses "mappers" to turn ColorPalettes into Schemes.
int seed = unchecked((int)0xFF5D5FDB);
// Construct a CorePalette
CorePalette corePalette = CorePalette.Of(seed);
// Get a mapper
DarkSchemeMapper mapper = new();
// Map
Scheme<int> scheme = mapper.Map(corePalette);
// Convert color type
Scheme<Color> schemeMaui = scheme.Convert(Color.FromInt);
Scheme<string> schemeString = scheme.Convert(x => "#" + x.ToString("X")[2..]);For custom colors, roles or mapping check out Extension.
Reduce the number of unique colors in an image. Multiple algorithms are available and they are used together.
Order colors returned by quantization based on suitability for theming and remove similar ones.
Shift a color towards the hue of the seed color.
using MaterialColorUtilities.Blend;
int harmonized = Blender.Harmonize(color, seedColor);Generate a seed color from an image (e.g. the user's wallpaper):
// Load the image into an int[].
// The image is stored in a resource embedded in the assembly, and then decoded and resized using SkiaSharp.
string imageResourceId = "MaterialColorUtilities.Console.Resources.wallpaper.webp";
using Stream resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(imageResourceId);
SKBitmap bitmap = SKBitmap.Decode(resourceStream).Resize(new SKImageInfo(112, 112), SKFilterQuality.Medium);
int[] pixels = bitmap.Pixels.Select(p => (int)(uint)p).ToArray();
// Run quantization and scoring and use the best color
int seedColor = ImageUtils.ColorsFromImage(pixels).First();Use that color to create a CorePalette;
CorePalette corePalette = new(seedColor);
int color = corePalette.Secondary[69];Turn the CorePalette into a Scheme<int> using a mapper:
Scheme<int> lightScheme = new LightSchemeMapper().Map(corePalette);
Scheme<int> darkScheme = new DarkSchemeMapper().Map(corePalette);Then convert your scheme to one with a different color type:
using Color = System.Drawing.Color;
Scheme<Color> lightSchemeColor = lightScheme.ConvertTo(Color.FromArgb);
Scheme<string> lightSchemeString = lightScheme.ConvertTo(x => "#" + x.ToString("X")[2..]);The same code can be found in the Playground.Console project.
CorePalette:public class MyCorePalette : CorePalette
{
public TonalPalette Orange { get; set; }
public MyCorePalette(int seed) : base(seed)
{
// You can harmonize a color to make it closer to the seed color
int harmonizedOrange = Blender.Harmonize(unchecked(0xFFA500), seed);
Orange = TonalPalette.FromInt(harmonizedOrange);
}
}Scheme<TColor>public partial class MyScheme<TColor> : Scheme<TColor>
{
public TColor Orange { get; set; }
public TColor OnOrange { get; set; }
public TColor OrangeContainer { get; set; }
public TColor OnOrangeContainer { get; set; }
}A source generator will generate new converter methods automatically.
Make sure to mark the class
partialand don't nest it inside another class.If you get warning
CS8032: An instance of analyzer MaterialColorUtilities.Schemes.SchemeConverterGenerator cannot be created...your IDE/compiler doesn't have Rosyln 4.0, so the source generator won't work. Make sure you are using Visual Studio 2022, MSBuild 17 or .NET 6.
public class MyLightSchemeMapper : LightSchemeMapper<MyCorePalette, MyScheme<int>>
{
protected override void MapCore(MyCorePalette palette, MyScheme<int> scheme)
{
base.MapCore(palette, scheme);
scheme.Orange = palette.Orange[40];
scheme.OnOrange = palette.Orange[100];
scheme.OrangeContainer = palette.Orange[90];
scheme.OnOrangeContainer = palette.Orange[10];
// You can also override already mapped colors
scheme.Surface = palette.Neutral[100];
}
}
public class MyDarkSchemeMapper : DarkSchemeMapper<MyCorePalette, MyScheme<int>>
{
protected override void MapCore(MyCorePalette palette, MyScheme<int> scheme)
{
base.MapCore(palette, scheme);
scheme.Orange = palette.Orange[80];
scheme.OnOrange = palette.Orange[20];
scheme.OrangeContainer = palette.Orange[30];
scheme.OnOrangeContainer = palette.Orange[90];
}
}MyCorePalette myCorePalette = new(seedColor);
MyScheme<string> myDarkScheme = new MyDarkSchemeMapper()
.Map(myCorePalette)
.ConvertTo(StringUtils.HexFromArgb);The same code can be found in the Playground.Console project.
For more info and samples check out the source code and the playground projects. If you have questions use the Discussions tab.
If you have found a bug or need a new feature, go ahead and open an issue.