Styling & Slots

Coralite components render directly in the Light DOM while maintaining strict style encapsulation and declarative slot projection. This guide explains Light DOM style scoping, root selector rewrites, reactive custom property theming, slot projections, AST slot transformations, and client slot helpers.

1. Light DOM & Scoping #

Unlike traditional Web Component frameworks, Coralite components do not use Shadow DOM. Instead, components render directly in the standard Light DOM.

To avoid style leakage without the restrictions of Shadow DOM encapsulation, Coralite's build-time PostCSS compiler scopes component styles into the CSS cascade layer (@layer components). This architecture guarantees two major advantages:

css
Code copied!
  /* Emitted component style block in CSS output */
  @layer components {
    @scope (:where(user-card)) to (slot, :scope [data-cid]) {
      :scope {
        display: block;
        padding: 1rem;
      }
      .title {
        font-weight: bold;
      }
    }
  }

2. :host and :host-context() #

Standard CSS pseudo-classes :host and :host-context() are fully supported in Coralite component stylesheets. At build time, Coralite's compiler rewrites these Shadow DOM selectors into standard Light DOM custom element selectors.

html
Code copied!
  <template id="user-card">
    <div class="content">
      <h3 class="title"></h3>
      <slot></slot>
    </div>
  </template>
  
  <style>
    /* Targets the host custom element */
    :host {
      display: block;
      border: 1px solid #e2e8f0;
      border-radius: 0.5rem;
      padding: 1rem;
    }
  
    /* Parameterized host variant */
    :host(.featured) {
      border-color: #3b82f6;
      box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.1);
    }
  
    /* Ancestor context styling */
    :host-context(.dark) {
      background-color: #1e293b;
      color: #f8fafc;
    }
  
    .title {
      font-size: 1.25rem;
    }
  </style>

3. Theming with Custom Properties #

Component theming in Coralite is driven via CSS Custom Properties (--custom-props). The top-level style block in defineComponent allows binding reactive CSS variables directly to the host custom element.

The style block contains functions that receive context (e.g. ({ state }) => ... or (context) => context.state...) and return CSS property values. Returning null, undefined, or an empty string removes the inline variable from the host element.

javascript
Code copied!
  import { defineComponent } from 'coralite'
  
  export default defineComponent({
    attributes: {
      active: Boolean,
      themeColor: { type: String, default: '#0284c7' }
    },
    style: {
      // Bind reactive CSS variables to the host tag using destructured context
      '--card-bg': ({ state }) => state.active ? '#f0fdf4' : '#ffffff',
      '--card-accent': ({ state }) => state.themeColor,
      '--card-border': ({ state }) => state.active ? '#22c55e' : '#e2e8f0'
    }
  })

4. Slot Projection #

Coralite components support standard default <slot></slot> and named slots (such as <slot name="title"></slot>). Slotted markup passed between component host tags is projected into the corresponding slot location.

To ensure layout consistency across parent containers, Coralite injects display: contents; onto slot containers at the root stylesheet level. This makes slot wrapper elements layout-transparent, allowing projected consumer children to participate directly in parent CSS Grid or Flexbox layouts.

html
Code copied!
  &lt;!-- Component Definition (dialog-box.html) --&gt;
  &lt;template id="dialog-box"&gt;
    &lt;div class="dialog"&gt;
      &lt;header class="dialog-header"&gt;
        &lt;slot name="title"&gt;&lt;/slot&gt;
      &lt;/header&gt;
      &lt;div class="dialog-body"&gt;
        &lt;slot&gt;&lt;/slot&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/template&gt;
  
  &lt;!-- Page Usage --&gt;
  &lt;dialog-box&gt;
    &lt;h2 slot="title"&gt;Confirm Action&lt;/h2&gt;
    &lt;p&gt;Are you sure you want to proceed with this update?&lt;/p&gt;
  &lt;/dialog-box&gt;

5. Transforming Slots #

The slots block in defineComponent allows you to intercept and transform projected Light DOM slot nodes before rendering. Each slot transformation function receives a context object containing { nodes, state, isServer, signal }, which can be destructured or accepted as a whole parameter.

The nodes array represents the collection of projected DOM nodes for that slot. You can filter, reorder, or modify nodes in place. On the client, returning undefined preserves existing DOM nodes and skips unnecessary re-transformation passes.

javascript
Code copied!
  import { defineComponent } from 'coralite'
  
  export default defineComponent({
    slots: {
      // Filter default slot nodes to include only paragraph elements
      default: ({ nodes, state }) =&gt; {
        return nodes.filter(node =&gt; {
          if (node.nodeType === 1) { // Element node
            return node.tagName.toLowerCase() === 'p'
          }
          return false
        })
      },
  
      // Transform named slot content
      title: ({ nodes }) =&gt; {
        return nodes.map(node =&gt; {
          if (node.nodeType === 1) {
            node.classList.add('heading-styled')
          }
          return node
        })
      }
    }
  })

6. The slots Helper in client() #

In the client() block, Coralite provides a slots helper object for imperative slot node inspection and query utilities:

javascript
Code copied!
  client: ({ slots, signal }) =&gt; {
    // Inspect slotted content imperatively
    if (slots.has('header')) {
      console.log('Header slot has', slots.count('header'), 'nodes')
      const headerNodes = slots.get('header')
      headerNodes.forEach(node =&gt; node.classList?.add('hydrated-slot'))
    }
  }

7. Lazy Dependency Stylesheets #

When a component or page depends on third-party CSS (such as a map, chart, or rich editor library) that should only load when the component mounts in the browser, use standard dynamic await import('*.css', options) inside client():

javascript
Code copied!
  export async function client ({ refs, signal }) {
    const [, L] = await Promise.all([
      import('leaflet/dist/leaflet.css', { media: 'screen', signal }),
      import('leaflet')
    ])
    L.map(refs('map'))
  }

Coralite bundles local and bare-package CSS files at build time with esbuild and PostCSS into hashed assets under assets/css/, injects a global <link rel="stylesheet"> into <head> in deterministic manifest order at runtime, deduplicates concurrent callers across components, and resolves to { default: link.sheet, link }.


Next: Return to the Guide Overview to explore more Coralite guides and tutorials.

Start Building with Coralite!

Use the scaffolding script to get jump started into your next project with Coralite

Copied commandline!