CustomElements
ElementInternals and CustomStateSet: a component's own state exposed to CSS as :state(name), plus the ARIA semantics it declares for itself.
@inject Bit.Butil.CustomElements customElementsMDN reference
Note:
Why a class isn't good enough
.is-loading shares a namespace with every other class on the page and can be
overridden by accident. :state(loading) belongs to the element and cannot be set from
outside it - which is what a component library wants.
Warning:
Define the tag before rendering it
Internals can only be attached from inside a custom element's constructor, so Butil defines a
minimal element for you - it does nothing but keep its internals. Call
Define before
the element is rendered; an element already in the DOM is upgraded, but any state set before the
upgrade is lost. A definition lasts for the document's lifetime and can never be replaced.
IsSupported reports ElementInternals.states. IsDefined answers whether a tag name has already been taken - by Butil or by anything else.
@inject Bit.Butil.CustomElements customElements
var supported = await customElements.IsSupported();
var defined = await customElements.IsDefined("butil-panel");
Live sample
support check output
Results will appear here when you interact with the samples.
Define the tag, render it, then toggle states on it. The CSS is the other half of the sample: a state you add turns the panel's border, with no class involved and nothing a page stylesheet could set by accident.
Bit.Butil.CustomElements customElements
<butil-panel @ref="_panel">Its border colour comes from :state(...).</butil-panel>
<button @onclick='() => AddState("loading")'>Loading</button>
<button @onclick='() => DeleteState("loading")'>Done</button>
{
private ElementReference _panel;
// Internals can only be attached from a custom element's constructor, so the tag has to be
// defined before it is upgraded. An element already in the DOM is upgraded when the definition
// arrives, but any state set before that upgrade is lost.
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender) await customElements.Define("butil-panel");
}
private async Task AddState(string state) => await customElements.AddState(_panel, state);
private async Task DeleteState(string state) => await customElements.DeleteState(_panel, state);
}butil-panel {
display: block;
padding: 1rem;
border: 2px solid var(--border, #ccc);
border-radius: 6px;
}
/* The point of a state over a class: :state() belongs to the element and cannot be set from
outside it, so no page stylesheet can put the panel into a state it is not in. */
butil-panel:state(loading) { border-color: #c98a00; }
butil-panel:state(ready) { border-color: #2e9e4f; }
Live sample
A
<butil-panel>. Its border colour comes from :state(…).
state output
Results will appear here when you interact with the samples.
ARIA set through internals is a default: an author attribute on the element still wins, which is exactly what makes it safe for a reusable component to state its own semantics rather than forcing them on the consumer.
await customElements.SetAria(_panel, "role", "region");
await customElements.SetAria(_panel, "ariaLabel", "Results");
await customElements.SetAria(_panel, "ariaBusy", "true");
Live sample
A second
<butil-panel>, this one for the ARIA calls. Inspect it in the
accessibility tree to see the defaults arrive.
aria output
Results will appear here when you interact with the samples.
API reference
Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()True when the runtime implements ElementInternals.states.
IsDefined
ValueTask<bool> IsDefined(string tagName)Whether a tag name has already been defined - by Butil or by anything else.
Define
ValueTask<bool> Define(string tagName)Defines a minimal custom element that carries internals. True when the tag is defined, including when it already was.
HasInternals
ValueTask<bool> HasInternals(ElementReference element)Whether this element was upgraded by a Butil definition and therefore has internals.
AddState
ValueTask<bool> AddState(ElementReference element, string state)Adds a state, matched in CSS as :state(name).
DeleteState
ValueTask<bool> DeleteState(ElementReference element, string state)Removes a state.
HasState
ValueTask<bool> HasState(ElementReference element, string state)Whether a state is set.
GetStates
ValueTask<string[]> GetStates(ElementReference element)Every state currently set on the element.
ClearStates
ValueTask<bool> ClearStates(ElementReference element)Removes every state.
SetAria
ValueTask<bool> SetAria(ElementReference element, string property, string? value)Sets an ARIA default through the element's internals. An author attribute still wins.