loading
Note:
Why the browser's parser and not a filter in C# A hand-written filter has to predict how the browser will parse mangled markup, and that gap is exactly where filters are defeated. Here the parser doing the work is the browser's, so what survives is what that browser would have parsed. SetHtml on an element already sanitizes; this is the same filter, configurable, reusable, and able to hand back a string.
Warning:
Unsupported means null, never the input Support is recent - Chromium first, others following. When the API is missing, Sanitize returns null rather than the markup untouched, so an unsupported browser can never be handed the input as if it had been cleaned. Check IsSetHtmlSupported and have a server-side path for the rest.

Support check

IsSupported / IsSetHtmlSupported

Two different questions. IsSetHtmlSupported asks for the sanitizing sink, which is all that sanitizing with the default configuration needs. IsSupported asks for the configurable Sanitizer object, which is what Create needs - a browser can have the first without the second.

C#
@inject Bit.Butil.Sanitizer sanitizer

var canSanitize = await sanitizer.IsSetHtmlSupported();
var canConfigure = await sanitizer.IsSupported();
Live sample
support check output
Results will appear here when you interact with the samples.

Sanitize to a string

Sanitize

Parse untrusted markup, drop what isn't safe, and get the result back as text - before storing it, indexing it or sending it on. The markup is parsed into a detached element that is never in the document, so nothing in it loads, runs or is announced on the way through.

C#
var clean = await sanitizer.Sanitize(userHtml);

if (clean is null)
{
    // no sanitizing sink in this browser - fall back to the server
}
Live sample
sanitize output
Results will appear here when you interact with the samples.

A configuration you can reuse

Create / SanitizerHandle

A configuration is either an allow-list or a deny-list, not both: setting Elements means 'only these', setting RemoveElements means 'the baseline, minus these'. Building a sanitizer is the expensive half of sanitizing one short fragment, so build it once per policy - 'what a comment may contain' - and reuse the handle for every comment.

C#
await using var comments = await sanitizer.Create(new SanitizerConfig
{
    Elements = ["p", "b", "i", "em", "strong", "a", "ul", "ol", "li"],
    Attributes = ["href", "title"],
    Comments = false,
});

// Null when the browser has no configurable Sanitizer - the unconfigured sink is still there:
var clean = comments is null
    ? await sanitizer.Sanitize(userHtml)
    : await comments.Sanitize(userHtml);
Live sample
configured output
Results will appear here when you interact with the samples.

Straight into an element

SanitizeInto

The same filter writing into an element rather than returning a string - what SetHtml does, with a configuration when you pass a handle. The result below is rendered live: the script and the event handler in the hostile sample are gone from it.

Razor
@inject Bit.Butil.Sanitizer sanitizer

<div @ref="preview"></div>

@code {
    private ElementReference preview;
    private SanitizerHandle? commentsHandle;   // from sanitizer.Create

    // The markup never comes back to C# in between, so there is no window in which the raw string
    // could be written somewhere else by accident.
    private async Task Show(string userHtml)
    {
        await sanitizer.SanitizeInto(preview, userHtml);

        // or under a configuration:
        await commentsHandle!.SanitizeInto(preview, userHtml);
    }
}
Live sample
The sanitized markup will be rendered here.
render output
Results will appear here when you interact with the samples.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes the configurable Sanitizer constructor. Returns default (false) during prerender/SSR instead of throwing.
IsSetHtmlSupported
ValueTask<bool> IsSetHtmlSupported()
True when the runtime exposes Element.setHTML - all that sanitizing without a configuration needs.
Sanitize
ValueTask<string?> Sanitize(string html)
Sanitizes markup with the browser's default configuration. Null when there is no sanitizing sink - never the input unchanged.
SanitizeInto
ValueTask<bool> SanitizeInto(ElementReference element, string html)
Sanitizes markup straight into an element. False when there is no sanitizing sink; the element is left alone.
Create
ValueTask<SanitizerHandle?> Create(SanitizerConfig config)
Builds a reusable sanitizer. Null when the runtime has no configurable Sanitizer or the configuration is invalid.
GetDefaultConfig
ValueTask<JsonElement?> GetDefaultConfig()
The browser's default configuration, expanded. Raw JSON: what comes back is not the shape a configuration goes in as.
DisposeAsync
ValueTask DisposeAsync()
On scope/circuit teardown, drops any sanitizers whose handle was never disposed.
SanitizerHandle.Sanitize
ValueTask<string?> Sanitize(string html)
Sanitizes markup under this configuration.
SanitizerHandle.SanitizeInto
ValueTask<bool> SanitizeInto(ElementReference element, string html)
Sanitizes markup into an element under this configuration.
SanitizerHandle.GetConfig
ValueTask<JsonElement?> GetConfig()
This configuration as the browser expanded it - the answer to 'is that element really allowed'.
SanitizerHandle.DisposeAsync
ValueTask DisposeAsync()
Drops the sanitizer. Calling it again does nothing.
SanitizerConfig
Elements | RemoveElements | ReplaceWithChildrenElements | Attributes | RemoveAttributes | Comments | DataAttributes
Which elements and attributes survive. An allow-list or a deny-list, not both; every property left null keeps the browser's default.
An unhandled error has occurred. Reload 🗙