Architecture
How BlazOrbit expresses style through data-attributes and CSS custom properties — and why that means no class juggling at runtime.
The IHas* interfaces
Components advertise their capabilities by implementing marker / property interfaces from
BlazOrbit.Core.Abstractions.Behaviors. A component that has a border
parameter implements IHasBorder; one that shows a loading indicator implements
IHasLoading. These interfaces are discovered by reflection at render time and
transformed into DOM attributes on the <bob-component> root.
public partial class MyCustom : BOBComponentBase,
IHasSize, IHasBorder, IHasLoading
{
[Parameter] public BOBSize Size { get; set; } = BOBSize.Medium;
[Parameter] public BorderStyle? Border { get; set; }
[Parameter] public bool Loading { get; set; }
}The data-bob-* attributes
State-like capabilities flip data-bob-* attributes on the root. CSS selectors
key off those attributes — never class toggles. The result is a DOM you can reason about
just by looking at it.
<bob-component
data-bob-component="button"
data-bob-size="large"
data-bob-loading="true"
data-bob-disabled="true"
style="--bob-inline-background: var(--palette-success); --bob-inline-color: #fff;">
…
</bob-component>
Canonical names live in FeatureDefinitions.DataAttributes. A few of the most common:
data-bob-component— kebab name of the component (e.g.button)data-bob-variant— the active variantdata-bob-size—small / medium / largedata-bob-density—compact / standard / comfortabledata-bob-disabled,data-bob-loading,data-bob-error,data-bob-active,data-bob-readonly,data-bob-required,data-bob-fullwidth,data-bob-shadow,data-bob-transitions
The --bob-inline-* variables
Value-carrying capabilities (colors, borders, shadows…) flow as CSS custom properties on
the element's style attribute. Component CSS consumes them through a
private alias pattern so defaults cascade cleanly.
bob-component[data-bob-component="button"] {
--_button-background: var(--bob-inline-background, var(--palette-primary));
--_button-color: var(--bob-inline-color, var(--palette-primary-contrast));
}
bob-component[data-bob-component="button"] button {
background-color: var(--_button-background);
color: var(--_button-color);
}
That way a consumer can override a single component instance by passing
BackgroundColor="@PaletteColor.Success", or re-skin the whole app by
setting --palette-primary on :root.
Families
Components that share a DOM / CSS contract belong to a family. Membership is a marker interface — the attribute is emitted automatically.
IInputFamilyComponent→[data-bob-input-base]→_input-family.css. Text, number, dropdown, textarea, color, date, checkbox, radio, switch.IPickerFamilyComponent→[data-bob-picker]→_picker-family.css. Color picker, date picker.IDataCollectionFamilyComponent→[data-bob-data-collection]→_data-collection-family.css. DataGrid, DataCards.
Family CSS provides the shared layout scaffolding (e.g. bob-input__wrapper /
__field / __label / __outline), so you don't need to
restyle every input component separately.
Why this design
Three practical consequences fall out of the approach:
- Zero runtime styling cost. No JS needed to toggle classes per interaction — browsers already match on attribute selectors.
- Debuggable. The DOM tells you every state a component is in. No hidden refs, no stale class lists.
- One override surface. Every visual knob is either a parameter (per-instance) or a CSS custom property (global). No CSS specificity war.