Component Styles
A component can declare CSS through the style option.
Udodi treats component styles as component-owned CSS and associates non-empty styles with a scope. When the component is mounted, the runtime marks the component's DOM boundary so the registered CSS can be applied to that component tree without becoming ordinary global CSS.
Defining Styles
The style option accepts a CSS string.
import { createComponent, css, html, render } from "udodi";
const Card = createComponent({
name: "Card",
style: css`
.card {
padding: 1rem;
border: 1px solid #e5e5e5;
border-radius: 8px;
}
.title {
margin: 0 0 0.5rem;
font-size: 1.125rem;
}
`,
template: html`
<article class="card">
<h2 class="title">Hello</h2>
<p>Body</p>
</article>
`,
});
render(Card(), "#app");The style value is CSS text.
The css tagged template is a convenience helper for writing CSS inside JavaScript. It returns the CSS text and does not introduce a separate CSS runtime or CSS-in-JS representation.
You can therefore also write:
style: `
.card {
padding: 1rem;
}
`,However, css`...` is preferred when writing component CSS because it gives editors and tooling a clear CSS template context.
Component Style Registration
Component styles are associated with the component when the component definition is created.
For a component with non-empty CSS, Udodi creates a scope for that style and registers the CSS with the runtime style system.
The important distinction is:
Component definition
│
│ non-empty style
▼
style registration
│
▼
scope ID
│
▼
mount()
│
▼
scoped component DOMThe CSS is not inserted into the DOM separately for every component instance.
Instead, the style registration is shared by the component definition's scope.
This allows multiple instances of the same component to use the same registered CSS.
Empty and Missing Styles
A component does not receive a style scope when it has no CSS.
These are equivalent from the perspective of component styling:
createComponent({
template: html`<div></div>`,
});and:
createComponent({
style: "",
template: html`<div></div>`,
});An empty style does not create any CSS scope.
CSS Scoping
Udodi uses CSS @scope to associate component CSS with the component's mounted DOM boundary.
Conceptually, a component style such as:
.card {
padding: 1rem;
}is placed inside a scope similar to:
@scope ([ud-scope-start="42"]) to ([ud-scope-end="42"]) {
.card {
padding: 1rem;
}
}The exact scope identifier is generated by Udodi and must not be written manually.
You should therefore write ordinary CSS selectors:
style: css`
.card {
padding: 1rem;
}
.title {
font-weight: 600;
}
button:hover {
opacity: 0.9;
}
`,Do not write:
[ud-scope-start="42"] .card {
/* Do not do this. */
}The runtime establishes the scope boundary for you.
Scope Boundaries
When a styled component is mounted, Udodi marks its component DOM boundary with a generated scope identifier.
Conceptually:
<div ud-scope-start="42">
<!-- component DOM -->
</div>The corresponding CSS scope uses that marker as its scope root.
For nested component trees, Udodi also establishes the appropriate scope boundary so a parent component's scoped rules do not behave like unrestricted global CSS inside a nested component.
This is important because component composition should not cause a parent's selectors to become effectively global.
Nested Components
Every component definition that contains non-empty CSS has its own style scope.
For example:
const Child = createComponent({
name: "Child",
style: css`
.label {
color: blue;
}
`,
template: html`
<span class="label">Child</span>
`,
});
const Parent = createComponent({
name: "Parent",
style: css`
.label {
color: red;
}
`,
template: html`
<div>
<span class="label">Parent</span>
${Child()}
</div>
`,
});The parent and child have separate style scopes.
Conceptually:
Parent scope
└── Parent DOM
├── .label ← Parent styles apply
│
└── Child scope
└── .label ← Child styles applyThe purpose of the boundary is to prevent the parent's component CSS from leaking into the child's component tree as though it were global CSS.
The child establishes its own styling boundary.
Styling the Component Root
A component's root element is the CSS @scope root. To style the root element itself, use the :scope pseudo-class:
const Card = createComponent({
name: "Card",
style: css`
:scope {
padding: 1rem;
border: 1px solid #e5e5e5;
border-radius: 8px;
}
.title {
margin: 0 0 0.5rem;
font-size: 1.125rem;
}
`,
template: html`
<article>
<h2 class="title">Hello</h2>
<p>Body</p>
</article>
`,
});Inside a component's style, :scope matches the component's root element (the element that receives the ud-scope-start marker).
You can still style descendants with ordinary selectors:
style: css`
:scope {
/* styles the component root */
display: block;
padding: 1rem;
}
.title {
/* styles descendants inside the scope */
font-weight: 600;
}
`,You do not need to add a generated scope class or attribute to the element yourself.
The runtime manages the scope markers.
Multiple Component Instances
A component definition can be mounted more than once:
render(Card(), "#first");
render(Card(), "#second");The style definition belongs to the component definition, not to an individual rendered instance.
Consequently, Udodi does not need to register a separate copy of the same CSS for every instance.
Conceptually:
Card definition
│
└── one registered style scope
│
├── Card instance #1
│
└── Card instance #2This avoids repeatedly inserting identical component CSS as instances are mounted.
Shared Style Element
Registered component styles are collected by Udodi's runtime style system.
The runtime uses a shared stylesheet element:
<style id="udodi-styles">
...
</style>The stylesheet contains the registered component scopes.
Conceptually:
Component styles
│
├── Card scope
├── Button scope
├── Dialog scope
└── ...
│
▼
#udodi-stylesThis means component styles do not require a separate <style> element for every component instance.
The shared stylesheet is updated when registered component styles need to be rendered.
Style Registration and Mounting
Style registration and DOM mounting are separate operations.
The component definition provides the CSS and its associated scope.
The mounting system is responsible for placing the scope boundary on the component's DOM.
Conceptually:
createComponent()
│
└── register component style
│
▼
scope ID
│
│
mount()
│
├── create component DOM
│
└── apply scope boundaryThis separation is important because a component definition can exist before it is mounted.
The CSS registration does not mean that a component instance has already been mounted.
Writing Selectors
Write selectors exactly as you normally would write CSS.
style: css`
.card {
padding: 1rem;
}
.title {
margin-bottom: 0.5rem;
}
button {
cursor: pointer;
}
button:hover {
opacity: 0.9;
}
`,There is no need to prefix selectors with a generated scope identifier.
Avoid unnecessary global selectors when the intent is component-local styling.
Prefer:
.card {
/* ... */
}over relying heavily on broad element selectors such as:
div {
/* ... */
}Although the component scope limits where the rule applies, explicit component classes communicate styling intent more clearly.
Selector Semantics
Component scoping limits the scope in which selectors are matched. It does not rewrite selectors into generated class names.
For example:
.title {
font-weight: 600;
}remains a .title selector inside the generated @scope rule.
This is different from CSS Modules-style transformations such as:
.title_xyz123 {
font-weight: 600;
}Udodi does not require generated class names in component templates.
Your template can continue to use:
<h2 class="title">Hello</h2>Dynamic Styling
The style option is intended for static component CSS. It is not a reactive style declaration.
For styles that change because of component state or computed values, use the template styling directives.
@class
Use @class when the styling change is represented by a class.
Static classes (applied once):
<div class="card" @class="'featured elevated'"></div>Dynamic classes use space-separated bindings. A binding can be:
- a conditional:
condition=>'className'(adds the class when the condition is truthy) - an expression that evaluates to a class string or array of class names
null/undefined/""(removes class previously contributed by that binding)
<div
class="card"
@class="featured=>'featured' sizeClass"
>
Content
</div>The component CSS can then define the variant:
style: css`
.card {
padding: 1rem;
}
.card.featured {
border-color: #4f46e5;
}
`,For the full directive syntax, binding forms, and edge cases, see Class.
@style
Use @style when values should be applied as inline styles.
Static styles (applied once):
<div @style="'color:red;background:blue'"></div>Dynamic styles use space-separated bindings. Each binding may evaluate to:
- a CSS declaration string (
"opacity:0.5;color:red") - an object (
{ opacity: 0.5, color: "red" }) - an array of pairs (
[["opacity","0.5"],["color","red"]]) null/undefined/""(removes styles previously contributed by that binding)
<div
class="box"
@style="boxStyles"
></div>// in component state / computed
boxStyles() {
return {
opacity: this.opacity,
transform: `scale(${this.scale})`,
};
}For the full directive syntax, supported return values, and merge behavior, see Style.
The distinction is:
| Mechanism | Use |
|---|---|
style: | Static component CSS |
@class | Reactive class changes |
@style | Reactive inline style changes |
Example: Component Variant
A common pattern is to keep the component's structural CSS static and use @class for state-driven variants.
import { createComponent, css, html, render } from "udodi";
const Card = createComponent({
name: "Card",
state() {
return {
featured: false,
};
},
methods: {
toggle() {
this.featured = !this.featured;
},
},
style: css`
.card {
padding: 1rem;
border: 1px solid #ddd;
border-radius: 8px;
}
.card.featured {
border-color: #4f46e5;
box-shadow: 0 0 0 1px #4f46e5;
}
`,
template: html`
<article
class="card"
@class="featured=>'featured'"
>
<p>Content</p>
<button @on="click=toggle">
Toggle featured
</button>
</article>
`,
});
render(Card(), "#app");The responsibilities are clear:
style
└── static CSS rules
@class
└── reactive class stateComponent Styles Are Not Global CSS
Component styles should be used for rules owned by the component.
For example:
style: css`
.dialog {
padding: 1rem;
}
.dialog-title {
font-size: 1.25rem;
}
`,Global application CSS should remain in the application's normal stylesheet.
Global CSS is a better place for:
- CSS resets
- typography defaults
- design tokens
- CSS custom properties shared by the application
- application-wide layout rules
- third-party styles
- global utility classes
For example:
:root {
--color-primary: #4f46e5;
--spacing-unit: 0.25rem;
}A component can then consume those shared variables:
style: css`
.button {
color: var(--color-primary);
padding: calc(var(--spacing-unit) * 4);
}
`,This keeps component ownership separate from application-wide design infrastructure.
CSS Custom Properties
Component styles can use normal CSS custom properties.
style: css`
.card {
color: var(--card-color, #222);
background: var(--card-background, white);
}
`,Because CSS custom properties participate in normal CSS inheritance, they can also be supplied by an ancestor when that is appropriate for the application's styling architecture.
Component scoping does not turn CSS custom properties into component-local state. They remain CSS variables governed by the browser's CSS cascade and inheritance rules.
Browser Support
Udodi's component-style isolation relies on CSS @scope.
Therefore, component styles require a browser environment that supports the CSS @scope feature.
Udodi does not emulate @scope by rewriting every selector into generated class names.
If support for older browsers is required, account for that at the application/browser compatibility level rather than assuming the component-style runtime provides a CSS @scope polyfill.
css Tagged Template
The css helper is intentionally lightweight.
style: css`
.button {
padding: 0.5rem 1rem;
}
`,It is useful because editors can recognize the contents as CSS.
It does not:
- create a stylesheet independently;
- generate class names;
- perform CSS Modules transformations;
- make CSS reactive;
- provide a separate CSS-in-JS object model.
The runtime ultimately works with the resulting CSS string.
Scope IDs
Scope identifiers are runtime-generated.
You should never depend on a particular numeric scope ID:
<!-- Do not depend on this value. -->
<div ud-scope-start="42">The generated identifier is an implementation detail used to establish the CSS scope boundary.
Do not:
- hard-code scope IDs in application CSS;
- query component elements by their generated scope ID;
- build application logic around the value;
- assume a specific numbering sequence.
Use normal component markup and selectors instead.
Scope Markers
The runtime uses scope markers to establish the component's CSS boundary.
These markers are runtime metadata rather than application markup.
Conceptually:
scope start
│
▼
┌───────────────┐
│ Component DOM │
│ │
│ .card │
│ .title │
│ button │
│ │
└───────────────┘
│
▼
scope boundaryThey exist so the CSS @scope rule can identify the component's DOM region. Application code should not manipulate these markers directly.
Styling Nested Component Trees
Consider:
const Child = createComponent({
name: "Child",
style: css`
.title {
color: blue;
}
`,
template: html`
<h2 class="title">Child</h2>
`,
});
const Parent = createComponent({
name: "Parent",
style: css`
.title {
color: red;
}
`,
template: html`
<section>
<h1 class="title">Parent</h1>
${Child()}
</section>
`,
});The two .title selectors are associated with different component scopes.
The parent selector is intended for the parent's component tree, while the child's own scope governs the child component.
This is the primary purpose of the component-style boundary: component composition should not turn every component's CSS into one unrestricted global stylesheet.
When to Use Component Styles
Use style when CSS is tightly associated with the component:
const Button = createComponent({
style: css`
.button {
display: inline-flex;
align-items: center;
}
`,
});Component styles are particularly useful for:
- component layout
- component-specific visual presentation
- component states and variants
- component-specific animations
- component-specific responsive rules
When to Use Global Styles
Use global styles when the rule belongs to the application rather than a single component.
Examples include:
* {
box-sizing: border-box;
}
body {
margin: 0;
}
:root {
--primary-color: #4f46e5;
}Global styles are also appropriate for:
- resets
- normalization
- design tokens
- global typography
- shared layout primitives
- application-wide utilities
Do not duplicate global infrastructure across component style declarations.
Complete Example
import { createComponent, css, html, render } from "udodi";
const Button = createComponent({
name: "Button",
state() {
return {
active: false,
};
},
methods: {
toggle() {
this.active = !this.active;
},
},
style: css`
.button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid #d1d5db;
border-radius: 6px;
background: white;
cursor: pointer;
}
.button.active {
background: #4f46e5;
color: white;
border-color: #4f46e5;
}
.button:hover {
opacity: 0.9;
}
`,
template: html`
<button
class="button"
@class="active=>'active'"
@on="click=toggle"
>
Button
</button>
`,
});
render(Button(), "#app");Here:
stylecontains static component CSS;.button.activedescribes a component variant;@classreacts to component state;- the runtime scopes the CSS to the component;
- the generated scope markers do not need to appear in the template.
Constraints
| Constraint | Behavior |
|---|---|
style accepts CSS text | The component receives its CSS as a string |
| css`...` is a helper | It provides CSS template syntax but does not create a separate CSS runtime |
| Empty/missing style | No component style scope is needed |
| Non-empty style | The runtime associates the CSS with a generated scope |
| Scope IDs | Generated by Udodi; application code must not depend on their values |
| CSS isolation | Implemented using CSS @scope and runtime scope boundaries |
| Stylesheet storage | Component styles are collected in the shared Udodi stylesheet |
| Multiple instances | Reuse the registered component style rather than requiring one stylesheet per instance |
| Nested components | Component scope boundaries prevent parent component CSS from behaving as unrestricted global CSS in child component trees |
| Dynamic styling | Use @class and @style for reactive styling; the component style option is static and is not reactive |
| Global CSS | Use application stylesheets for resets, tokens, and shared global rules |
| Browser support | Requires CSS @scope support |
Minimal Example
import { createComponent, css, html, render } from "udodi";
const Hello = createComponent({
name: "Hello",
style: css`
.hello {
font-weight: 600;
}
`,
template: html`
<p class="hello">Hello</p>
`,
});
render(Hello(), "#app");The .hello selector is scoped to the Hello component. No generated scope selector needs to be added to the template.
Summary
Udodi component styling follows a simple model:
style: css`...`
│
▼
CSS string
│
▼
component scope
│
▼
shared Udodi stylesheet
│
▼
mounted component boundary
│
▼
scoped CSSThe component owns its CSS, while the runtime owns the mechanics required to scope and register it.
Application code should therefore focus on normal CSS:
style: css`
.card {
padding: 1rem;
}
.card.featured {
border-color: blue;
}
`,and normal template bindings:
<article
class="card"
@class="featured=>'featured'"
>rather than managing scope identifiers or runtime scope markers directly.