{ } Lips v0.2.0

Styling

Scoped stylesheets

A component’s stylesheet is scoped to that component. Lips stamps a rel attribute on the component’s root elements and prefixes every rule with a matching attribute selector.

const card = {
  default: `
    <div class="card">
      <h2 class="title">{state.title}</h2>
      <div class="body">{state.body}</div>
    </div>`,

  stylesheet: `
    .card { border: 1px solid #eee; border-radius: 4px; padding: 1rem }
    .title { font-size: 1.2rem; margin-bottom: .5rem }
    .body { color: #666 }`
}

lips.register('card', card)

Rendered markup and injected CSS:

<div rel="card" class="card">
  <h2 class="title">Title</h2>
  <div class="body">Body</div>
</div>
[rel="card"] .card { border: 1px solid #eee; border-radius: 4px; padding: 1rem }
[rel="card"] .title { font-size: 1.2rem; margin-bottom: .5rem }
[rel="card"] .body { color: #666 }

Two components can use .card for entirely different things without colliding.

The scope name is the registered name — or the name passed to lips.render( name, … ). Every instance of a component shares one injected <style> element, reference-counted so it is removed only when the last instance is destroyed.

Dynamic styles

Bind classes and inline styles like any other attribute:

<!-- conditional class via interpolation -->
<div class="item {state.active ? 'active' : ''}"></div>

<!-- or a full expression -->
<div class=(state.active ? 'item active' : 'item')></div>

<!-- inline style -->
<div style="color: {state.color}; font-size: {state.size}px"></div>

<!-- computed from a handler -->
<div style=self.rowStyle( row )></div>

<!-- attribute removed entirely when falsy -->
<button disabled=!state.valid>Submit</button>
handler: {
  rowStyle( row ){
    return `background: ${row.done ? '#f6f6f6' : 'transparent'}`
  }
}

CSS nesting and preprocessing

Stylesheets run through Stylis, so nesting, &, and media queries all work:

stylesheet: `
  .card {
    border: 1px solid #eee;
    border-radius: 4px;
    padding: 1rem;

    .title {
      font-size: 1.2rem;

      &.highlighted { font-weight: 600 }
    }

    &.active { border-color: #333 }
    &:hover { box-shadow: 0 2px 4px rgba(0,0,0,.1) }
  }

  @media (max-width: 768px) {
    .card { padding: .5rem }
  }`

CSS custom properties

Custom properties are the simplest way to theme, because they cross the scope boundary naturally:

const app = {
  state: { dark: false },

  default: `
    <div class="app {state.dark ? 'dark' : ''}">
      <button on-click( () => state.dark = !state.dark )>Toggle theme</button>
      <card/>
    </div>`,

  stylesheet: `
    .app {
      --fg: #222;
      --bg: #fff;
      color: var(--fg);
      background: var(--bg);
    }
    .app.dark {
      --fg: #eee;
      --bg: #111;
    }`
}

A child component can read var(--fg) even though the property was declared by an ancestor’s scoped sheet — custom properties inherit through the DOM, not through the scope.

Global styles

Lips scopes every component stylesheet. For genuinely global rules — a reset, a font import, a third-party sheet — use an ordinary <link> or <style> in your HTML, or import the CSS through your bundler.

<head>
  <link rel="stylesheet" href="/reset.css">
</head>

External style files

With a bundler that can import CSS as a string, keep styles in their own file:

import cardCss from './card.css?raw'

export default {
  stylesheet: cardCss,
  default: `<div class="card">…</div>`
}

The @lipsjs/lips/runtime build ships no CSS preprocessor — it is tree-shaken out along with the template compiler. A component with a source stylesheet logs a warning and skips injection there. Precompiled apps that need scoped styles should either use the full entry or ship their CSS as static files.