Concept

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.

MyCustom.razor.cs
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.

Rendered DOM
<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 variant
  • data-bob-sizesmall / medium / large
  • data-bob-densitycompact / standard / comfortable
  • data-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.

Component CSS
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.