loading
Note:
Computed is not inline The element extensions read and write inline style - what an element was told. This reads what it actually is: the answer after the cascade, inheritance, and the browser resolving relative units into pixels. A width set in em comes back in px; a colour set as a keyword comes back as rgb().

Support check

IsSupported / IsSupportsAvailable / IsRegisterPropertyAvailable / IsConstructableStyleSheetAvailable / IsHighlightAvailable / IsTypedOmAvailable

getComputedStyle 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.

C#
@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();
Live sample
support check output
Results will appear here when you interact with the samples.

What an element really is

GetComputedStyle / GetComputedStyleValue / GetComputedStyleAll

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.

C#
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");
Live sample
A sample element, styled by the page's own stylesheet.
computed style output
Results will appear here when you interact with the samples.

Asking the parser

Supports / SupportsCondition / Escape

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.

C#
@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}");
Live sample
supports output
Results will appear here when you interact with the samples.

A stylesheet of your own

CreateStyleSheet / InsertRule / DeleteRule / GetRules / Replace

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.

Razor
@implements IAsyncDisposable
@inject Bit.Butil.Css css

@code {
    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();
    }
}
Live sample
stylesheet output
Results will appear here when you interact with the samples.

Animating a custom property

RegisterProperty

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.

C#
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) }
Live sample
a transition on a custom property
register output
Results will appear here when you interact with the samples.

Highlighting without touching the DOM

HighlightText / ClearHighlight

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.

Razor
@inject Bit.Butil.Css css

<p @ref="_prose">The text to search through.</p>

@code {
    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");
    }
}
Live sample

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.

highlight output
Results will appear here when you interact with the samples.

Read computed values as numbers

ElementReference.GetComputedValue / GetComputedProperties

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.

Razor
<div @ref="_box" style="width:320px">...</div>

@code {
    private ElementReference _box;

    private async Task Measure()
    {
        var width = await _box.GetComputedValue("width");
        // width.Value == 320, width.Unit == "px"
    }
}
Live sample
A box to measure.
typed computed style output
Results will appear here when you interact with the samples.

Write inline styles without string building

ElementReference.SetStyleValue / SetStyleText / GetStyleValue / DeleteStyleValue / ClearStyleValues

SetStyleValue 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.

Razor
<div @ref="_box">...</div>

@code {
    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");
    }
}
Live sample
A box to style.
inline style output
Results will appear here when you interact with the samples.

Houdini worklets

SupportsPaintWorklet / SupportsLayoutWorklet / AddPaintWorklet / AddLayoutWorklet

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.

@inject Bit.Butil.Css css

<div id="checkered">...</div>

@code {
    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");
        }
    }
}
Live sample
worklet output
Results will appear here when you interact with the samples.
Warning:
Reading a style costs something Computing a style forces layout if anything has changed since the last one, which is why reading a style inside a loop that also writes one is the classic way to make a page crawl. Read what you need in one call, then write.
Warning:
The typed style maps are Chromium only Where 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

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes getComputedStyle. Returns default (false) during prerender/SSR instead of throwing.
IsSupportsAvailable / IsRegisterPropertyAvailable / IsConstructableStyleSheetAvailable / IsHighlightAvailable
ValueTask<bool> ...()
The per-feature checks. CreateStyleSheet works where constructable sheets are missing, by appending a style element instead.
IsTypedOmAvailable
ValueTask<bool> IsTypedOmAvailable()
True when the runtime implements the CSS Typed OM's unit factories (CSS.px and friends), which is what gates the style-map element extensions.
GetComputedStyle
ValueTask<Dictionary<string, string>?> GetComputedStyle(ElementReference element, string[] properties, string? pseudoElement = null)
Resolved values for the named properties, in CSS spelling. A pseudo-element like ::before is only readable this way.
GetComputedStyleValue
ValueTask<string> GetComputedStyleValue(ElementReference element, string property, string? pseudoElement = null)
One resolved value, or an empty string when it is not set.
GetComputedStyleAll
ValueTask<Dictionary<string, string>?> GetComputedStyleAll(ElementReference element, string? pseudoElement = null)
Every resolved property - some 350 of them. For diagnostics rather than for reading two values.
Supports / SupportsCondition
ValueTask<bool> Supports(string property, string value), SupportsCondition(string condition)
Asks the parser whether it understands a pair, or a whole @supports condition.
Escape
ValueTask<string> Escape(string value)
Makes a string safe to use in a selector. Anything that came from data rather than your markup should go through it.
RegisterProperty
ValueTask<string?> RegisterProperty(string name, string syntax = "*", bool inherits = false, string? initialValue = null)
Teaches the browser what a custom property holds, which is what lets it be animated. Null on success, or the reason it failed.
RegisterProperty
ValueTask<bool> RegisterProperty(CssPropertyDefinition definition)
The same registration from an object. False when already registered, the initial value doesn't parse, or there is no registerProperty here.
SupportsPaintWorklet / SupportsLayoutWorklet
ValueTask<bool> SupportsPaintWorklet(), SupportsLayoutWorklet()
True when the runtime implements the Houdini paint / layout worklet.
AddPaintWorklet
ValueTask<bool> AddPaintWorklet(string url)
Loads a paint worklet module.
AddLayoutWorklet
ValueTask<bool> AddLayoutWorklet(string url)
Loads a layout worklet module.
CreateStyleSheet
ValueTask<StyleSheetHandle?> CreateStyleSheet()
A stylesheet of your own, already in the document.
HighlightText
ValueTask<int> HighlightText(string name, ElementReference element, string search, bool caseSensitive = false)
Highlights every occurrence without touching the DOM. Returns the count, or -1 where the API is missing. Needs a ::highlight(name) rule to be visible.
ClearHighlight
ValueTask ClearHighlight(string name)
Removes a highlight by name.
StyleSheetHandle.InsertRule / DeleteRule / GetRules / Replace
ValueTask<int> InsertRule(string rule, int index = -1); ValueTask<bool> DeleteRule(int index); ValueTask<string[]> GetRules(); ValueTask<bool> Replace(string css)
Editing the sheet. InsertRule answers -1 for a rule the parser rejected. Replace is simpler than tracking indices.
ElementReference.GetComputedValue
ValueTask<CssValue?> GetComputedValue(string property)
Reads a computed value as a number and a unit.
ElementReference.GetComputedProperties
ValueTask<string[]> GetComputedProperties()
Every property name in the element's computed style map.
ElementReference.GetStyleValue
ValueTask<CssValue?> GetStyleValue(string property)
Reads an inline style value as a number and a unit.
ElementReference.SetStyleValue
ValueTask<bool> SetStyleValue(string property, double value, string unit = 'px')
Writes an inline style value as a typed number.
ElementReference.SetStyleText
ValueTask<bool> SetStyleText(string property, string value)
Writes an inline style value from text, with validation.
ElementReference.DeleteStyleValue
ValueTask<bool> DeleteStyleValue(string property)
Removes one inline style property.
ElementReference.ClearStyleValues
ValueTask<bool> ClearStyleValues()
Removes every inline style property.
ElementReference.GetStyleProperties
ValueTask<string[]> GetStyleProperties()
Every property name currently set inline.
ElementReference.IsStyleMapSupported
ValueTask<bool> IsStyleMapSupported()
True when the runtime implements the CSS Typed OM.
An unhandled error has occurred. Reload 🗙