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.