Documentation Index

Fetch the complete documentation index at: https://docs.unqork.io/llms.txt

Use this file to discover all available pages before exploring further.

Styling Guide - BYO

Prev Next

Learn how to write maintainable, encapsulated styles for BYO custom components.

Style Encapsulation

Shadow DOM provides true style encapsulation—styles inside don't leak out, styles outside don't leak in.

Project structure:

src/
├── MyButton/
│   ├── MyButton.ts
│   ├── MyButton.css
│   └── index.ts

MyButton.css:

/* These styles are scoped to this component */
button {
  background: #0066cc;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;

  /* PostCSS nesting support */
  &:hover {
    background: #0052a3;
  }

  &:disabled {
    opacity: 0.5;
  }
}

MyButton.ts:

import styles from './MyButton.css?raw'

// Define styles once, reuse across all instances
const sheet = new CSSStyleSheet()
sheet.replaceSync(styles)

export class MyButton extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: 'open' })
  }

  connectedCallback() {
    this.shadowRoot!.adoptedStyleSheets = [sheet]
    this.shadowRoot!.innerHTML = `<button><slot></slot></button>`
  }
}

Note: Using PostCSS with nesting and other features requires configuration. See CSS Framework Support section below.

Benefits:

  • ✅ No class name collisions with platform or other components.
  • ✅ Can use simple, semantic selectors without needing BEM.
  • ✅ Styles are bundled with the component.
  • ✅ Prevents accidental style pollution.

Considerations:

  • Global styles like fonts and CSS variables don't automatically inherit.
  • Some CSS frameworks might not work as expected.
  • Requires understanding of Shadow DOM APIs.

Without Shadow DOM (Platform Styles)

If you need to integrate with platform styles, use namespaced BEM class names to prevent collisions. See BEM Class Naming for detailed guidance.

Platform styles:

.acme-task-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  /* Inherits platform fonts and CSS variables */
  font-family: var(--platform-font-family, inherit);
}

.acme-task-list__item {
  display: flex;
  align-items: center;
  padding: 12px;
}

.acme-task-list__title {
  flex: 1;
}

.acme-task-list__item--completed .acme-task-list__title {
  text-decoration: line-through;
  opacity: 0.6;
}

TaskList.ts:

export class TaskList extends HTMLElement {
  private _items: Task[] = []

  connectedCallback() {
    this.render()
  }

  render() {
    this.innerHTML = `
      <div class="acme-task-list">
        ${this._items.map(item => `
          <div class="acme-task-list__item ${item.completed ? 'acme-task-list__item--completed' : ''}">
            <span class="acme-task-list__title">${item.title}</span>
          </div>
        `).join('')}
      </div>
    `
  }
}

Note: The platform is responsible for loading TaskList.css. The component only renders markup with BEM class names.

Why use platform styles:

  • ✅ Inherits platform fonts, colors, and CSS variables automatically.
  • ✅ Integrates with the global design system.
  • ✅ CSS file is separate and easier to maintain.
  • ✅ Can leverage platform utility classes.
  • ✅ Works with all CSS frameworks and tooling.

Implications of not using Shadow DOM:

Platform Styles (no Shadow DOM) Shadow DOM
✅ Inherits platform fonts, CSS variables, themes ⚠️ Must explicitly inherit or redefine
✅ Can use platform utility classes ❌ Cannot access global styles
✅ Integrates with global design system ❌ Fully isolated
❌ Global styles can interfere with your component ✅ Styles are isolated
❌ Your styles can leak to other components ✅ No style leakage
❌ Must use BEM/namespacing to prevent collisions ✅ Can use simple selectors
✅ Works with all CSS frameworks ⚠️ Some frameworks might not penetrate the shadow boundary

BEM Class Naming

When not using Shadow DOM, use BEM (Block Element Modifier) methodology to write maintainable, collision-free class names.

What Is BEM?

BEM is a naming convention that makes class names predictable and self-documenting. It structures class names into three parts:

.block__element--modifier
  • Block: Standalone component (.card, .button, .task-list).
  • Element: Part of a block (.card__header, .button__icon, .task-list__item).
  • Modifier: Variation or state (.card--featured, .button--disabled, .task-list__item--completed).

Syntax Rules

Block:

.block-name { }
  • Describes what it is (.menu, .search-form, .product-card).
  • Use lowercase and hyphens for multi-word names.
  • No prefixes except namespace.

Element:

.block-name__element-name { }
  • Describes its role in the block.
  • Double underscore __ separates block from element.
  • Cannot exist outside the block.
/* ✅ Good */
.task-list__item { }
.task-list__title { }
.task-list__checkbox { }

/* ❌ Bad - element without block */
.item { }
.title { }

Modifier:

.block-name--modifier-name { }
.block-name__element-name--modifier-name { }
  • Describes state, variation, or behavior.
  • Double hyphen -- separates from block/element.
  • Applied alongside the base class.
<!-- ✅ Good - both classes -->
<div class="task-list__item task-list__item--completed">

<!-- ❌ Bad - modifier alone -->
<div class="task-list__item--completed">

Best Practices

Namespace your components (Required):

Always prefix your BYO component class names with your company or project name to prevent collisions with platform styles or other BYO components. This is not optional—every BYO component should be namespaced.

/* ✅ Good - company/project namespace */
.acme-task-list { }
.acme-task-list__item { }
.acme-task-list__item--completed { }

.mycompany-button { }
.mycompany-button__icon { }
.mycompany-button--primary { }

/* ❌ Bad - no namespace, will collide */
.task-list { }
.button { }

Recommended namespace patterns, listed in priority order:

  1. Company name: .acme-*, .mycompany-*, .contoso-*
  2. Project name: .myproject-*, .client-portal-*
  3. Feature area: .analytics-*, .workflow-*, .reporting-*
  4. Component library: .ui-*, .core-*, .custom-*

Why namespacing is critical:

  • ✅ Prevents conflicts with Unqork platform styles.
  • ✅ Prevents conflicts with other BYO components.
  • ✅ Makes ownership clear in browser DevTools.
  • ✅ Enables safe global CSS updates without breaking your components.
  • ✅ Follows industry best practices for third-party components.

Keep selectors flat:

/* ✅ Good - flat BEM selectors */
.acme-card { }
.acme-card__header { }
.acme-card__title { }
.acme-card__body { }
.acme-card--featured { }

/* ❌ Bad - nested selectors */
.acme-card .header { }
.acme-card .header .title { }
.acme-card.featured { }

Don't chain elements:

/* ✅ Good */
.acme-card__header { }
.acme-card__header-title { }

/* ❌ Bad - no .block__element__element */
.acme-card__header__title { }

If you need deeper nesting, create a new element name that describes the full context.

Use meaningful names:

/* ✅ Good - describes purpose */
.acme-product-card__price { }
.acme-product-card__add-button { }
.acme-product-card--on-sale { }

/* ❌ Bad - generic or positional */
.acme-product-card__text { }
.acme-product-card__button-1 { }
.acme-product-card--blue { }

Avoid presentation-based names:

/* ✅ Good - semantic */
.alert--warning { }
.button--primary { }
.text--error { }

/* ❌ Bad - presentational */
.alert--yellow { }
.button--blue { }
.text--red { }

semantic names survive design changes; presentational names don't.

Common Patterns

State modifiers:

.acme-button { }
.acme-button--disabled { }
.acme-button--loading { }
.acme-button--active { }
<button class="acme-button acme-button--disabled">Disabled</button>
<button class="acme-button acme-button--loading">Loading...</button>

Variant modifiers:

.acme-button { }
.acme-button--primary { }
.acme-button--secondary { }
.acme-button--ghost { }
<button class="acme-button acme-button--primary">Primary</button>
<button class="acme-button acme-button--ghost">Ghost</button>

Size modifiers:

.acme-button { }
.acme-button--small { }
.acme-button--large { }
<button class="acme-button acme-button--small">Small</button>
<button class="acme-button acme-button--large">Large</button>

Combining modifiers:

<!-- ✅ Good - multiple modifiers -->
<button class="acme-button acme-button--primary acme-button--large acme-button--loading">
  Submit
</button>

When to Use BEM

Use BEM when:

  • Not using Shadow DOM to allow platform style inheritance.
  • Styles might conflict with platform or other components.
  • Building reusable component libraries.
  • Need predictable, maintainable class names.

Don't use BEM when:

  • Using Shadow DOM, where simple selectors work fine.
  • Writing utility classes (.hidden, .text-center).
  • One-off styles that won't be reused.

Examples

Task list component with namespace:

/* Block */
.acme-task-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Elements */
.acme-task-list__item {
  display: flex;
  align-items: center;
  padding: 12px;
  background: white;
  border: 1px solid #ddd;
}

.acme-task-list__checkbox {
  margin-right: 12px;
}

.acme-task-list__title {
  flex: 1;
}

.acme-task-list__delete-button {
  margin-left: auto;
}

/* Modifiers */
.acme-task-list__item--completed {
  opacity: 0.6;
}

.acme-task-list__item--completed .acme-task-list__title {
  text-decoration: line-through;
}

.acme-task-list__delete-button--disabled {
  pointer-events: none;
  opacity: 0.3;
}

Component markup:

export class TaskList extends HTMLElement {
  render() {
    this.innerHTML = `
      <div class="acme-task-list">
        ${this._items.map(item => `
          <div class="acme-task-list__item ${item.completed ? 'acme-task-list__item--completed' : ''}">
            <input type="checkbox" class="acme-task-list__checkbox" ${item.completed ? 'checked' : ''}>
            <span class="acme-task-list__title">${item.title}</span>
            <button class="acme-task-list__delete-button">Delete</button>
          </div>
        `).join('')}
      </div>
    `
  }
}

CSS Framework Support

PostCSS transforms your CSS at build time, enabling modern CSS features like imports, nesting, and autoprefixing. It's the recommended approach for sharing design tokens across component stylesheets.

Features:

  • CSS imports: Share tokens with @import.
  • Nesting: Write nested selectors like Sass/LESS.
  • Autoprefixer: Vendor prefixes added automatically.
  • No runtime overhead: All transformations happen at build time.
  • Works with Shadow DOM: Processed CSS works everywhere.

Installation:

npm install -D postcss-import postcss-nesting autoprefixer

Configuration:

// vite.config.ts
import { defineConfig } from 'vite'
import postcssImport from 'postcss-import'
import postcssNesting from 'postcss-nesting'
import autoprefixer from 'autoprefixer'

export default defineConfig({
  css: {
    postcss: {
      plugins: [postcssImport(), postcssNesting(), autoprefixer()],
    },
  },
})

See Sharing Styles Across Components for detailed examples of using PostCSS to share design tokens.

Comparison: PostCSS vs. Plain CSS

Feature PostCSS Plain CSS
Setup complexity Medium (requires config) None
Build-time processing ✅ Yes ❌ No
Runtime overhead ❌ None ❌ None
Dynamic styles ❌ No ⚠️ Limited (CSS variables)
CSS imports/composition ✅ Yes (@import) ❌ No (with ?raw)
Nesting ✅ Yes ❌ No
Autoprefixing ✅ Yes ❌ No
Unique class names ❌ Manual (BEM) ❌ Manual (BEM)
Bundle size impact Medium (inlined imports) Smallest
Learning curve Low None
Best for Sharing tokens, modern CSS features Simple static styles

Decision guide:

  • Use Plain CSS when:

    • The component has simple, static styles.
    • No need to share styles across components.
    • Want the smallest bundle size.
    • Prefer a standard CSS workflow.
  • Use PostCSS when:

    • Need to share design tokens across components.
    • Want modern CSS features like nesting and imports.
    • Need autoprefixing for browser compatibility.
    • Styles are static but benefit from composition.

Sharing Styles Across Components

With ?raw imports, you can't use CSS @import in the traditional way. Here are practical approaches to share design tokens and common styles across multiple BYO components.

PostCSS transforms your CSS at build time, enabling features like imports, custom properties, nesting, and autoprefixing. Use it to share design tokens across component stylesheets.

Project structure:

src/
├── tokens.css
├── Button/
│   ├── Button.ts
│   ├── Button.css
│   └── index.ts
├── Card/
│   ├── Card.ts
│   ├── Card.css
│   └── index.ts
└── entry.ts

tokens.css:

/* Global design tokens */
:root {
  --color-primary: #0066cc;
  --color-surface: white;
  --color-text: #212529;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --radius: 4px;
}

Button.css:

@import './tokens.css';

button {
  background: var(--color-primary);
  padding: var(--spacing-sm) var(--spacing-md);
  border-radius: var(--radius);
  color: white;
  border: none;

  /* Nesting support with postcss-nesting */
  &:hover {
    opacity: 0.9;
  }
}

Button.ts:

import styles from './Button.css?raw'

const sheet = new CSSStyleSheet()
sheet.replaceSync(styles)

export class Button extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: 'open' })
  }

  connectedCallback() {
    this.shadowRoot!.adoptedStyleSheets = [sheet]
    this.shadowRoot!.innerHTML = `<button><slot></slot></button>`
  }
}

Note: PostCSS installation and configuration is shown in the CSS Framework Support section above.

After processing, Button.css?raw contains the inlined tokens, prefixed CSS, and expanded nesting.

Benefits:

  • Shared tokens: Import tokens into every component stylesheet at build time.
  • CSS imports: Compose styles from multiple files.
  • Nesting: Write nested selectors like in Sass/LESS.
  • Autoprefixer: Vendor prefixes added automatically.
  • No runtime overhead: All transformations happen at build time.
  • Works with Shadow DOM: Processed CSS works everywhere.
  • Standard CSS output: Final output is pure CSS.

Tradeoffs:

  • ❌ Requires build configuration.
  • ❌ Tokens duplicated in each component bundle.
  • ❌ No dynamic styles—everything is static at build time.

Approach 2: JavaScript Token Constants

Export design tokens as JavaScript constants for use in CSS-in-JS or template literals.

tokens.js or tokens.ts:

export const tokens = {
  color: {
    primary: '#0066cc',
    surface: 'white',
    text: '#212529',
  },
  spacing: {
    sm: '8px',
    md: '16px',
  },
  radius: '4px',
}

Button.js using tokens:

import { tokens } from '../tokens'

const styles = new CSSStyleSheet()
styles.replaceSync(`
  button {
    background: ${tokens.color.primary};
    padding: ${tokens.spacing.sm} ${tokens.spacing.md};
    border-radius: ${tokens.radius};
    color: white;
    border: none;
  }
`)

export class Button extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: 'open' })
  }

  connectedCallback() {
    this.shadowRoot!.adoptedStyleSheets = [styles]
    this.shadowRoot!.innerHTML = `<button><slot></slot></button>`
  }
}

Benefits:

  • ✅ Works with JavaScript or TypeScript.
  • ✅ Autocomplete in IDE using TypeScript or JSDoc.
  • ✅ Can compute values dynamically.
  • ✅ Shareable across components.
  • ✅ Type-safe when using TypeScript.

Comparison

Approach Shadow DOM Type Safety Bundle Size Dynamic Styles Use Case
PostCSS ✅ Yes ❌ No Medium ❌ No Shared tokens, nesting, autoprefixing
JavaScript Constants ✅ Yes ⚠️ Optional Small ✅ Yes Shared values, computed styles, dynamic styling

Recommended approach:

  • Use PostCSS to share design tokens across component stylesheets.
  • Use JavaScript constants when you need type safety or dynamic computed styles.
  • Combine both for maximum flexibility.

Summary

Takeaways:

  • Shadow DOM: Provides true style encapsulation so styles cannot leak in or out.
  • Platform styles: Use a separate .css file with BEM naming to integrate with global platform styles.
  • PostCSS: Share design tokens and use modern CSS features like imports, nesting, and autoprefixing across component stylesheets.
  • Constructable Stylesheets: Improve performance when using external CSS files in Shadow DOM.

Decision Matrix:

Use Case Recommended Approach
New isolated component Shadow DOM + Constructable Stylesheets
Need platform style inheritance Platform Styles (separate .css + BEM naming)
Share design tokens across components PostCSS with @import
Modern CSS features (nesting, autoprefixer) PostCSS
Dynamic styles based on component state CSS Variables + JavaScript
Large stylesheet (Shadow DOM) External .css file with Constructable Stylesheets
Large stylesheet (Light DOM) Platform Styles (separate .css + BEM)

Next Steps:


Changelog

Date Change
2026-05-15 Initial publication