Css
The CSS object model: what an element is actually rendered with, what the browser understands, stylesheets you can edit, highlights painted over text without touching the DOM, animatable custom properties, Houdini worklets, and the typed style maps that give you a length as a number instead of a string.
@inject Bit.Butil.Css cssMDN reference
em comes back in
px; a colour set as a keyword comes back as rgb().
Support check
IsSupported / IsSupportsAvailable / IsRegisterPropertyAvailable / IsConstructableStyleSheetAvailable / IsHighlightAvailable / IsTypedOmAvailablegetComputedStyle and CSS.supports are everywhere. registerProperty, constructable stylesheets, the Custom Highlight API and the Typed OM are newer. CreateStyleSheet works either way - where constructable sheets are missing it appends a style element instead.
@inject Bit.Butil.Css css
var computed = await css.IsSupported();
var supports = await css.IsSupportsAvailable();
var register = await css.IsRegisterPropertyAvailable();
var sheets = await css.IsConstructableStyleSheetAvailable();
var highlight = await css.IsHighlightAvailable();
var typedOm = await css.IsTypedOmAvailable();Property names in CSS spelling - 'font-size', not 'fontSize'. A list rather than the whole style object on purpose: a computed style has some 350 properties on it, and marshalling all of them to answer a question about two is most of the cost of the call. A pseudo-element is the one case where there is no element to reach for at all - '::before' is only readable this way.
private ElementReference _sample;
var values = await css.GetComputedStyle(_sample, ["color", "font-size", "padding-left"]);
var one = await css.GetComputedStyleValue(_sample, "background-color");
// generated content has no element - this is the only way to read its style
var before = await css.GetComputedStyle(_sample, ["content"], "::before");The honest way to feature-detect CSS: it asks the browser's own parser rather than inferring support from a name and a version. Escape is the other half - an id that starts with a digit, or contains a dot or a space, is perfectly legal HTML and illegal in a selector without it, so anything that came from data rather than from your markup should go through it.
@inject Bit.Butil.Dom dom
var grid = await css.Supports("display", "grid");
var has = await css.SupportsCondition("selector(:has(a))");
var safe = await css.Escape(idFromTheDatabase);
var element = await dom.Query($"#{safe}");Rules here reach the whole page through selectors, which is the thing setting a style on one element cannot do - a theme, a print stylesheet, a ::highlight() rule. A rule the parser cannot read is refused rather than ignored, which is more useful than a stylesheet quietly missing a line.
IAsyncDisposable
Bit.Butil.Css css
{
private StyleSheetHandle? _sheet;
private async Task Apply(string accent)
{
_sheet ??= await css.CreateStyleSheet();
await _sheet!.InsertRule(".css-sample { outline: 2px solid rebeccapurple }");
// or hand over the whole thing, which is simpler than tracking indices
await _sheet.Replace(BuildTheme(accent));
}
public async ValueTask DisposeAsync()
{
// Removes it from the document; a sheet nobody disposes outlives the component that made it.
if (_sheet is not null) await _sheet.DisposeAsync();
}
}An unregistered custom property is just a string to the browser, and strings do not interpolate - which is why a transition on --brand does nothing at all until it is registered. Registering says what it holds, and from then on it animates like any other value. Registering the same name twice fails, so do it once at startup. The CssPropertyDefinition overload is the same call with the arguments in an object, answering a bool instead of the reason.
var error = await css.RegisterProperty("--butil-accent", "<color>", inherits: false, initialValue: "#6366f1");
// or, as an object, when you only need a yes/no:
var registered = await css.RegisterProperty(new CssPropertyDefinition
{
Name = "--brand-hue",
Syntax = "<number>",
InitialValue = "210",
Inherits = true
});
// and now this transitions rather than snapping:
// .thing { transition: --butil-accent 400ms; background: var(--butil-accent) }Wrapping matches in <mark> mutates the DOM - which breaks a Blazor diff, invalidates anything measured around it, and has to be undone before the next search. A custom highlight is painted over the text and changes nothing underneath it. Nothing is visible until a ::highlight(name) rule exists, which is what the stylesheet above is for.
Bit.Butil.Css css
<p @ref="_prose">The text to search through.</p>
{
private ElementReference _prose;
private StyleSheetHandle? _sheet; // from "A stylesheet of your own"
private async Task Highlight(string term)
{
// Nothing is painted without a ::highlight() rule naming the same registry entry - the
// ranges exist either way, and are simply invisible.
await _sheet!.InsertRule("::highlight(search) { background: gold; color: black }");
var count = await css.HighlightText("search", _prose, term);
await css.ClearHighlight("search");
}
}The Custom Highlight API paints ranges over text without adding a single element to the document. Search this paragraph for a word - highlight, element, text - and notice that the markup underneath is the same markup it was before.
The Typed OM half. Computed, so this reflects the cascade and layout - a percentage width comes back resolved to pixels, as a Value and a Unit, rather than as the string '320px'. Non-numeric values (a keyword, a colour) come back with IsNumeric false and only Text filled in. Chromium only: where IsStyleMapSupported is false these return null.
<div @ref="_box" style="width:320px">...</div>
{
private ElementReference _box;
private async Task Measure()
{
var width = await _box.GetComputedValue("width");
// width.Value == 320, width.Unit == "px"
}
}Write inline styles without string building
ElementReference.SetStyleValue / SetStyleText / GetStyleValue / DeleteStyleValue / ClearStyleValuesSetStyleValue takes a number and a unit factory name (px, percent, deg, rem, fr, s…); SetStyleText takes text, for values that aren't a number and a unit. No chance of building a malformed declaration - an invalid value is rejected rather than silently ignored. Blazor owns the DOM it rendered and a diff can undo these on the next render - the same caveat as any inline-style write.
<div @ref="_box">...</div>
{
private ElementReference _box;
private async Task Write()
{
await _box.SetStyleValue("width", 60, "percent");
await _box.SetStyleText("border-color", "rebeccapurple");
var set = await _box.GetStyleValue("width");
await _box.DeleteStyleValue("width");
}
}A paint worklet draws a custom paint() image the way a canvas does, but as a live CSS value. It runs in its own global scope with no DOM - it receives the size and the custom properties it declared an interest in, and draws. The module is fetched as a real file, so it cannot be bundled into the app's own JS. The layout worklet is behind a flag even in Chromium.
Bit.Butil.Css css
<div id="checkered">...</div>
{
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender is false) return;
if (await css.SupportsPaintWorklet())
{
// Fetched as a real file - a paint worklet runs in its own global scope, so it cannot
// be bundled into the app's own JavaScript.
await css.AddPaintWorklet("/worklets/checkerboard.js");
}
}
}// No DOM in here, and no access to the page: the worklet is handed a size and the custom properties
// it declared an interest in, and draws.
class Checkerboard {
// Only the properties named here are passed in, and a change to one of them repaints.
static get inputProperties() { return ['--checker-size', '--checker-color']; }
paint(ctx, size, properties) {
const step = parseInt(properties.get('--checker-size').toString(), 10) || 16;
ctx.fillStyle = properties.get('--checker-color').toString().trim() || '#0f6cbd';
for (let y = 0; y < size.height; y += step) {
for (let x = 0; x < size.width; x += step) {
if ((x / step + y / step) % 2 === 0) ctx.fillRect(x, y, step, step);
}
}
}
}
// The name paint() addresses in CSS.
registerPaint('checkerboard', Checkerboard);/* The worklet is a live CSS value, not a one-off render: the element repaints when it resizes or
when one of the declared custom properties changes. */
#checkered {
--checker-size: 16;
--checker-color: #0f6cbd;
background-image: paint(checkerboard);
}IsTypedOmAvailable (or an element's IsStyleMapSupported) is false
the style-map methods return null or false, so the ordinary string-based style APIs remain the
portable choice. GetComputedStyle, Supports and Escape
work everywhere.
API reference
ValueTask<bool> IsSupported()ValueTask<bool> ...()ValueTask<bool> IsTypedOmAvailable()ValueTask<Dictionary<string, string>?> GetComputedStyle(ElementReference element, string[] properties, string? pseudoElement = null)ValueTask<string> GetComputedStyleValue(ElementReference element, string property, string? pseudoElement = null)ValueTask<Dictionary<string, string>?> GetComputedStyleAll(ElementReference element, string? pseudoElement = null)ValueTask<bool> Supports(string property, string value), SupportsCondition(string condition)ValueTask<string> Escape(string value)ValueTask<string?> RegisterProperty(string name, string syntax = "*", bool inherits = false, string? initialValue = null)ValueTask<bool> RegisterProperty(CssPropertyDefinition definition)ValueTask<bool> SupportsPaintWorklet(), SupportsLayoutWorklet()ValueTask<bool> AddPaintWorklet(string url)ValueTask<bool> AddLayoutWorklet(string url)ValueTask<StyleSheetHandle?> CreateStyleSheet()ValueTask<int> HighlightText(string name, ElementReference element, string search, bool caseSensitive = false)ValueTask ClearHighlight(string name)ValueTask<int> InsertRule(string rule, int index = -1); ValueTask<bool> DeleteRule(int index); ValueTask<string[]> GetRules(); ValueTask<bool> Replace(string css)ValueTask<CssValue?> GetComputedValue(string property)ValueTask<string[]> GetComputedProperties()ValueTask<CssValue?> GetStyleValue(string property)ValueTask<bool> SetStyleValue(string property, double value, string unit = 'px')ValueTask<bool> SetStyleText(string property, string value)ValueTask<bool> DeleteStyleValue(string property)ValueTask<bool> ClearStyleValues()ValueTask<string[]> GetStyleProperties()ValueTask<bool> IsStyleMapSupported()