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:
- Natural Encapsulation: Component selectors are scoped to the component root, preventing internal styles from leaking out into the parent page or sibling components.
- Utility Class Compatibility: Unlayered global utilities (such as Tailwind CSS or Bootstrap utility classes) take cascade priority over layered component styles without requiring
!importantrules.
/* 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.
:host— Targets the root custom element container (e.g.<user-card>).:host(.active)— Targets the root custom element when a specific class or attribute is present.:host-context(.dark)— Applies styles based on ancestor context (such as a.darkclass on<html>or<body>).
<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.
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.
<!-- Component Definition (dialog-box.html) -->
<template id="dialog-box">
<div class="dialog">
<header class="dialog-header">
<slot name="title"></slot>
</header>
<div class="dialog-body">
<slot></slot>
</div>
</div>
</template>
<!-- Page Usage -->
<dialog-box>
<h2 slot="title">Confirm Action</h2>
<p>Are you sure you want to proceed with this update?</p>
</dialog-box>
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.
import { defineComponent } from 'coralite'
export default defineComponent({
slots: {
// Filter default slot nodes to include only paragraph elements
default: ({ nodes, state }) => {
return nodes.filter(node => {
if (node.nodeType === 1) { // Element node
return node.tagName.toLowerCase() === 'p'
}
return false
})
},
// Transform named slot content
title: ({ nodes }) => {
return nodes.map(node => {
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:
slots.get('name')— Returns an array of projected DOM nodes for the specified slot name (or'default').slots.has('name')— Returns a boolean indicating whether content was provided for the specified slot.slots.count('name')— Returns the count of projected nodes inside the specified slot.
client: ({ slots, signal }) => {
// 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 => 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():
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.