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:

HTML
Code copied!
<!-- 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:

css
Code copied!
  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:

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

css
Code copied!
  /* 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:

4. Root Tokens & Slot Projections #

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.

HTML
Code copied!
<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.

HTML
Code copied!
<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.

HTML
Code copied!
<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 #

Global vs. Component Styles #

While component styles are scoped, you often need global styles for things like typography, resets, and utility classes.

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.

Start Building with Coralite!

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

Copied commandline!