Skip to content

Recommended CSS

Keep styling familiar: write native CSS alongside your HTML, and use bindings only for values that need to change.

Use ordinary stylesheets and native CSS. Brio does not compile, relocate, namespace, or restrict your CSS. Prefer shared external styles for global design tokens, resets, layers, and reusable classes.

Before Brio runs, the browser paints templates as authored: ${…} text, every b-if branch, and unpopulated lists. Add b-cloak to the mount root or any region, and put this rule in <head> so it applies before first paint:

<style>[b-cloak] { display: none !important; }</style>
<main id="app" b-cloak>…</main>
  • Hydration removes the cloak from each element: mount roots, rows, branches, and component templates. b-ignore content is never hydrated, so its cloak stays.
  • Unmount restores the original attributes, including b-cloak.
  • Include the CSS yourself, before first paint. Brio does not inject it; deferred or module scripts would inject it too late.

Cloaked content stays hidden if JavaScript fails or is disabled. Cloak only regions that are meaningless without Brio; keep essential content server-rendered or static.

Put a bare @scope in an inline style to scope its rules to that style element’s physical parent. No directive or data boundary is needed:

<div>
<style>
@scope { h2 { color: green; } }
</style>
<div>
<style>
@scope { h2 { color: red; } }
</style>
<h2>Red here!</h2>
</div>
<h2>Green here!</h2>
</div>

The inner rule wins through native scoping proximity when cascade priority and specificity tie. The scope root selector does not add specificity to its contained rules; explicit :scope adds pseudo-class specificity and targets the root itself. Use ordinary nesting, media/container queries, and @supports as supported by your browsers.

The same pattern works directly inside a component template: an inline style whose parent is the component host scopes to that host. Putting it deeper in a wrapper scopes to the wrapper. b-data changes expression context, not CSS scope. Projected slots follow physical placement for CSS while retaining their author expression context. A b-template wrapper disappears; use a surviving wrapper or explicit selector if the style needs a stable root.

Native CSS accepts @scope (.panel) to (.limit) { ... }. Roots are ordinary document selectors and can match multiple elements, not just the style’s parent. The limit excludes each matching element and its descendants. To style the limit element but not its children/subtrees, use to (.limit > *). With an implicit parent root, write @scope to (.limit) { ... }.

For an instance-specific explicit root, combine existing Brio bindings:

<section b-id="['style']" b-bind-class="{ [$id.style]: true }">
<style>
@scope (.${$id.style}) to (.limit) {
h2 { color: red; }
}
</style>
<h2>Red here</h2>
<div class="limit"><h2>Outside this rule's scope</h2></div>
</section>

b-id provides the unique name; the class binding and text interpolation use that same name. Brio adds no separate styling metadata.

Reactive values: prefer CSS custom properties

Section titled “Reactive values: prefer CSS custom properties”

Changing an inline style’s text updates its stylesheet. For changing values, prefer binding custom properties instead of regenerating CSS text:

<section b-data="{ accent: 'teal' }"
b-bind-style="{ '--accent': $data.accent }">
<style>
@scope {
:scope { border: 1px solid var(--accent); }
h2 { color: var(--accent); }
}
</style>
<h2>Reactive color</h2>
<button b-on-click="$data.accent = 'purple'">Change accent</button>
</section>

Ordinary <style> text supports the same ${...} interpolation as other hydrated text, including reactive references. Use it for authored selectors or structural CSS when needed; prefer simple references such as ${$id.style} or ${$data.selector} rather than expressions with nested braces. Brio does not escape/sanitize interpolated CSS. Do not insert untrusted values into CSS or executable templates. To keep ${...} literal, put b-ignore on the style element. b-text is also available when explicitly binding complete CSS text without secondary interpolation.

  • Scoped selectors are not Shadow DOM isolation. Global CSS can still match descendants; inherited properties and custom properties cross scope limits. Teleported content outside a scope stops matching its rules.
  • Keyframe and cascade-layer names follow native document-global CSS naming, even when authored inside @scope. Choose distinctive names such as my-app-card-pulse, and use CSS variables for animation parameters as needed. Brio does not rewrite animations or impose serialization restrictions.
  • Inline styles stay where authored, with their attributes and formatting. A hidden connected branch’s stylesheet remains active. A detached branch’s inline stylesheet disconnects and stops applying until reinsertion. This also affects rules targeting explicit roots or teleported elements elsewhere.
  • Default b-if keeps inline styles during leave transitions and removes them with the branch when leave completes. Managed insertion/removal likewise follows the actual DOM. Unmount stops Brio bindings and restores original markup; static CSS in that restored markup can remain active.
  • Dispose Brio scopes through its lifecycle APIs instead of deleting owned DOM manually. CSS connectivity is native, but reactive bindings still need cleanup.
  • Browser CSS parsing and support apply normally: there is no framework fallback or capability error for @scope. Support starts at Chromium 118, Firefox 146, and Safari 17.4, with a reported Safari 26.0–26.3 input/textarea regression fixed in 26.4. Check your target browsers and provide ordinary global-class styles when broader compatibility is required. CSP inline-style rules still apply.