Component styles
Coralite provides a powerful styling system that allows you to write scoped CSS directly within your components. This guide explores advanced styling techniques, including how scoping works and how to use modern CSS features like nesting.
Scoped Styles #
When you define styles inside a Coralite component template, they are automatically scoped to that component. This prevents styles from leaking out and affecting other parts of your application.
Coralite achieves this at build time by wrapping component styles in the components CSS cascade layer (@layer components) and leveraging modern CSS @scope with dual-scope partitioning:
- Standard Component Scope: Uses a "donut boundary" (
to (slot, :scope [data-cid], :is([is], c-token))) to strictly encapsulate standard component rules (likep,.btn,img), preventing them from leaking into nested child components or slotted consumer content. - Slotted Scope: Emitted only when
::slotted()rules are present. Omitsslotfrom the scope limit (to (:scope [data-cid], :is([is], c-token))) so transformed::slotted()Light DOM slot projections can target projected consumer elements while keeping child components encapsulated.
<!-- Source Component (my-card.html) -->
<template id="my-card">
<div class="card">
<h2 class="title">{{ title }}</h2>
<div class="body"><slot></slot></div>
</div>
</template>
<style>
:host {
display: block;
}
/* Standard rule: will NOT affect slotted <p> elements */
p {
color: #64748b;
}
/* Slotted rule: explicitly styles projected consumer <p> elements */
::slotted(p) {
font-size: 1rem;
line-height: 1.5;
}
</style>
During the build process, the component's styles are transformed into dual @scope blocks:
c-token { display: contents; }
@layer components {
@supports (@scope) {
/* Standard component scope: donut hole prevents styles from leaking into slots or child components */
@scope (:where(my-card)) to (slot, :scope [data-cid], :is([is], c-token)) {
:scope {
display: block;
}
p {
color: #64748b;
}
}
/* Slotted scope: allows explicit ::slotted() transforms to target projected children while encapsulating child components */
@scope (:where(my-card)) to (:scope [data-cid], :is([is], c-token)) {
:scope > slot > p, :scope > p[slot] {
font-size: 1rem;
line-height: 1.5;
}
}
}
@supports not (@scope) {
/* Fallback using zero-specificity :where() and CSS nesting */
:where(my-card) {
display: block;
p, :where(& > slot > p, & > p[slot]) {
color: #64748b;
}
}
}
}
Cascade Layers (@layer components) & Specificity #
By wrapping component styles inside @layer components and using :where() for tag selectors, Coralite solves two of the most common styling challenges in web component development:
1. Specificity Taming with :where() #
The :where() pseudo-class has 0 specificity (0, 0, 0). Wrapping the custom element selector in :where(my-button) ensures that component encapsulation never artificially inflates selector specificity.
An internal rule like .btn { ... } retains its normal single-class specificity (0, 1, 0) instead of being inflated to (0, 1, 1), keeping your CSS easy to override and predict.
2. Cascade Layer Priority (@layer components) #
According to the CSS Cascade Layers specification, unlayered CSS rules always take precedence over layered CSS rules, regardless of selector specificity.
Because Coralite places component styles into @layer components:
- Utility Classes Win: Unlayered utility classes (like Tailwind, Bootstrap, or custom utility classes such as
.hiddenor.p-4) naturally override component styles without needing!important. - Page-Level Overrides: Ad-hoc styles defined in page-level stylesheets or
<style>blocks apply over component styles cleanly.
3. Establishing Explicit Cascade Order #
You can explicitly control the layer evaluation order across your entire application by declaring a layer hierarchy at the top of your global CSS (e.g., in src/css/main.css):
/* Define explicit cascade order at the top of your global stylesheet */
@layer reset, base, components, utilities;
@layer reset {
* {
box-sizing: border-box;
margin: 0;
}
}
@layer utilities {
.hidden {
display: none;
}
.flex {
display: flex;
}
.text-center {
text-align: center;
}
}
In this hierarchy:
resetandbaseprovide low-priority defaults that component styles can effortlessly override.componentsautomatically receives all Coralite component styles.utilities(and any unlayered styles) cleanly override component styles without specificity battles.
4. Root Tokens & Slot Projections #
- Root
c-token: The rulec-token { display: contents; }is emitted outside the component layer at the root of the stylesheet. This ensures reactive attribute and slot placeholders remain layout-transparent and inert across all layers. - Light DOM Slot Projections (
::slotted()): When you style slotted content using::slotted(selector), Coralite transforms the selector into dual Light DOM branches (:scope > slot > selector, :scope > selector[slot]), keeping slot styles encapsulated within thecomponentslayer.
Styling the Root Custom Element (:host & :host-context) #
The preferred and canonical method for styling and modifying the root custom element container itself (e.g. <my-button> or <user-card>) is using standard CSS :host and :host-context() pseudo-class selectors.
Using :host eliminates the need for redundant inner wrapper <div> elements just to control display modes, borders, margins, or padding.
<template id="user-profile">
<div class="header">
<h2 class="name">{{ name }}</h2>
</div>
<div class="bio"><slot></slot></div>
</template>
<style>
/* 1. Preferred: Style the root custom element container itself */
:host {
display: block;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
margin-bottom: 1rem;
}
/* Parameterized variants based on host classes or attributes */
:host(.featured) {
border-color: #3b82f6;
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.1);
}
:host([disabled]) {
opacity: 0.6;
pointer-events: none;
}
/* Hover & focus states on the root element */
:host:hover {
border-color: #cbd5e1;
}
/* Contextual styling based on ancestor theme */
:host-context(.dark) {
background-color: #1e293b;
color: #f8fafc;
}
/* 2. Styling internal elements: :host is NOT required */
.header {
padding: 1rem;
background: #f1f5f9;
}
.name {
font-size: 1.25rem;
font-weight: bold;
}
</style>
When :host is NOT Required #
Important: To style scoped classes, elements, or children within the component template (such as .header or .name in the example above), :host is not required.
Coralite's compiler automatically scopes all standard descendant selectors (like .header { ... } or button { ... }) under the custom element's tag name during build time. You do not need to prefix internal classes with :host.
CSS Nesting #
Coralite supports CSS nesting syntax out of the box, allowing you to write cleaner and more hierarchical styles.
You can use the & selector to reference the parent selector.
The Parent Selector (&) #
The ampersand (&) refers to the parent rule's selector. It's particularly useful for pseudo-classes,
pseudo-elements, and modifiers.
<template id="styled-card">
<div class="card">
<div class="title">{{ title }}</div>
<slot></slot>
</div>
</template>
<style>
.card {
background: #fff;
padding: 1rem;
border: 1px solid #ddd;
/* Hover state */
&:hover {
border-color: #aaa;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* Dark theme modifier (if applying a class to the component) */
&.dark {
background: #333;
color: #fff;
}
/* Nested element */
& .title {
font-size: 1.25rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
}
</style>
This nesting capability makes it easy to encapsulate all styles related to a component within a readable structure.
Reactive Styling with the style Block #
In addition to static <style> tags, Coralite components can drive dynamic CSS variables and inline host styles reactively via the top-level style block in defineComponent.
The style block allows you to define getters that return CSS custom properties (prefixed with --) or standard CSS properties based on component state. These CSS variables naturally cascade down through your component's template.
<template id="theme-button">
<button class="btn">Click me</button>
</template>
<style>
.btn {
background-color: var(--btn-bg, #0284c7);
color: var(--btn-color, #ffffff);
padding: 0.5rem 1rem;
border-radius: 0.375rem;
}
</style>
<script type="module">
import { defineComponent } from 'coralite'
export default defineComponent({
attributes: {
variant: {
type: String,
default: 'primary',
values: ['primary', 'dark', 'outline']
}
},
style: {
// Dynamically drive CSS variables on the custom element host
'--btn-bg': (state) => {
if (state.variant === 'dark') {
return '#0f172a'
}
if (state.variant === 'outline') {
return 'transparent'
}
return '#0284c7'
},
'--btn-color': (state) => {
if (state.variant === 'outline') {
return '#0284c7'
}
return '#ffffff'
}
}
})
</script>
Style Removal & Normalization Rules #
- CSS Custom Properties: Property names starting with
--(e.g.'--btn-bg') are preserved exactly as-is. - Standard CSS Properties: Standard camelCase properties (e.g.
backgroundColor) are automatically normalized to kebab-case (e.g.background-color). - Property Removal: Returning
null,undefined,false, or an empty string''dynamically removes the inline property from the host tag. - Numeric Zero: The numeric value
0is strictly preserved as valid CSS (e.g.opacity: 0).
Global vs. Component Styles #
While component styles are scoped, you often need global styles for things like typography, resets, and utility classes.
- Global Styles: Define these in your main CSS/SCSS files (e.g.,
src/scss/main.scss) and include them in your layout or page templates. These styles apply to the entire document. - Component Styles: Define these within the
<template>tag using a<style>block. These are specific to the component and are automatically scoped.
You can use global utility classes (like Bootstrap or Tailwind) within your components alongside your scoped styles. Because component styles are automatically placed in @layer components, global utility classes and unlayered styles take precedence naturally without specificity conflicts.
Using Preprocessors #
If your project is configured to use Sass or PostCSS (via coralite-scripts), these transformations occur before the component styles are scoped.
This means you can use variables, mixins, and other preprocessor features directly within your component's <style> block.