Skip to content

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.

js
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:

js
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:

text
 Component definition
          │
          │ non-empty style
          ▼
  style registration
          │
          ▼
      scope ID
          │
          ▼
       mount()
          │
          ▼
 scoped component DOM

The 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:

js
createComponent({
  template: html`<div></div>`,
});

and:

js
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:

css
.card {
  padding: 1rem;
}

is placed inside a scope similar to:

css
@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:

js
style: css`
  .card {
    padding: 1rem;
  }

  .title {
    font-weight: 600;
  }

  button:hover {
    opacity: 0.9;
  }
`,

Do not write:

css
[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:

html
<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:

js
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:

text
Parent scope
└── Parent DOM
    ├── .label        ←  Parent styles apply
    │
    └── Child scope
        └── .label    ←  Child styles apply

The 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:

js
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:

js
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:

js
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:

text
Card definition
      │
      └── one registered style scope
              │
              ├── Card instance #1
              │
              └── Card instance #2

This 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:

html
<style id="udodi-styles">
  ...
</style>

The stylesheet contains the registered component scopes.

Conceptually:

text
Component styles
      │
      ├── Card scope
      ├── Button scope
      ├── Dialog scope
      └── ...
             │
             ▼
       #udodi-styles

This 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:

text
createComponent()
      │
      └── register component style
                  │
                  ▼
               scope ID
                  │
                  │
                mount()
                  │
                  ├── create component DOM
                  │
                  └── apply scope boundary

This 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.

js
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:

css
.card {
  /* ... */
}

over relying heavily on broad element selectors such as:

css
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:

css
.title {
  font-weight: 600;
}

remains a .title selector inside the generated @scope rule.

This is different from CSS Modules-style transformations such as:

css
.title_xyz123 {
  font-weight: 600;
}

Udodi does not require generated class names in component templates.

Your template can continue to use:

html
<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):

html
<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)
html
<div
  class="card"
  @class="featured=>'featured' sizeClass"
>
  Content
</div>

The component CSS can then define the variant:

js
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):

html
<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)
html
<div
  class="box"
  @style="boxStyles"
></div>
js
// 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:

MechanismUse
style:Static component CSS
@classReactive class changes
@styleReactive 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.

js
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:

text
style
  └── static CSS rules

@class
  └── reactive class state

Component Styles Are Not Global CSS ​

Component styles should be used for rules owned by the component.

For example:

js
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:

css
:root {
  --color-primary: #4f46e5;
  --spacing-unit: 0.25rem;
}

A component can then consume those shared variables:

js
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.

js
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.

js
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:

html
<!-- 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:

text
   scope start
        │
        ▼
┌───────────────┐
│ Component DOM │
│               │
│   .card       │
│   .title      │
│   button      │
│               │
└───────────────┘
        │
        ▼
  scope boundary

They 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:

js
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:

js
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:

css
* {
  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 ​

js
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:

  • style contains static component CSS;
  • .button.active describes a component variant;
  • @class reacts to component state;
  • the runtime scopes the CSS to the component;
  • the generated scope markers do not need to appear in the template.

Constraints ​

ConstraintBehavior
style accepts CSS textThe component receives its CSS as a string
css`...` is a helperIt provides CSS template syntax but does not create a separate CSS runtime
Empty/missing styleNo component style scope is needed
Non-empty styleThe runtime associates the CSS with a generated scope
Scope IDsGenerated by Udodi; application code must not depend on their values
CSS isolationImplemented using CSS @scope and runtime scope boundaries
Stylesheet storageComponent styles are collected in the shared Udodi stylesheet
Multiple instancesReuse the registered component style rather than requiring one stylesheet per instance
Nested componentsComponent scope boundaries prevent parent component CSS from behaving as unrestricted global CSS in child component trees
Dynamic stylingUse @class and @style for reactive styling; the component style option is static and is not reactive
Global CSSUse application stylesheets for resets, tokens, and shared global rules
Browser supportRequires CSS @scope support

Minimal Example ​

js
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:

text
 style: css`...`
     │
     ▼
 CSS string
     │
     ▼
 component scope
     │
     ▼
 shared Udodi stylesheet
     │
     ▼
 mounted component boundary
     │
     ▼
 scoped CSS

The component owns its CSS, while the runtime owns the mechanics required to scope and register it.

Application code should therefore focus on normal CSS:

js
style: css`
  .card {
    padding: 1rem;
  }

  .card.featured {
    border-color: blue;
  }
`,

and normal template bindings:

html
<article
  class="card"
  @class="featured=>'featured'"
>

rather than managing scope identifiers or runtime scope markers directly.