---
title: "Styling Components With css() and style Tags"
description: "Styling with css() for dynamic style values, plus component-owned style tags lifted at parse time and injected once per template. Single-file components."
source: https://lemonadejs.com/docs/styling/
---

# Styling and css()

Styling has two tools and two jobs. **`css()`** builds a style *value* from
an object,
and a **`<style>` tag inside `` html`` ``** ships a component's CSS in
its own source, lifted at parse time, injected once per template (as a
constructed stylesheet the document adopts):

```javascript
import { css } from 'lemonadejs';

const Chip = (props, { state }) => {
    const y = state(12);
    return html`<span class="lm-chip" style="${() => css({ top: y.value })}">
        <style>.lm-chip { padding: 2px 8px; border-radius: 12px; }</style>
        ${props.label}
    </span>`;
};
// → "top:12px" on the element; the rule adopted by the document, once
```

`css()` turns an object into a style string (numbers become px); the component owns its own `<style>`.

<!--example-->

```js
import { html, css } from 'lemonadejs';

const App = (props, { state }) => {
    const x = state(20);
    const hue = state(200);

    return html`<div class="ex-stage">
        <style>
            .ex-stage { position: relative; height: 110px; border: 1px dashed #bbb; border-radius: 8px; }
            .ex-chip { position: absolute; top: 20px; padding: 6px 14px; border-radius: 16px; color: #fff; transition: left .15s; }
            .ex-stage input { position: absolute; bottom: 10px; width: 40%; }
        </style>
        <span class="ex-chip" style="${() => css({ left: Number(x.value) + '%', background: 'hsl(' + hue.value + ' 70% 45%)' })}">chip</span>
        <input type="range" min="0" max="85" bind="${x}" style="left:5%" />
        <input type="range" min="0" max="360" bind="${hue}" style="right:5%" />
    </div>`;
};
```

## Background

Two challenges recur in component styling: handling dynamic values reliably, and keeping styles colocated with the component that owns them.

- **String-built style attributes.** Hand-concatenating
  `'top:' + y + 'px;left:' + x + 'px'` is the zero-tooling approach, and
  it carries predictable slips: a forgotten unit, a forgotten semicolon,
  a `false &&` conditional stringified into the attribute. None of these
  produce an error; the browser drops what it cannot parse.
- **The distribution question.** A component that ships as
  `index.js` + `style.css` has two artifacts to keep in sync, an extra
  import (or `<link>`) for every consumer, and a failure mode, styles
  forgotten, that renders as a working but unstyled component.

## How LemonadeJS solves it

**`css()` is a value helper, not a styling engine.** It takes an object
with typed keys and returns the style string:

```javascript
css({ top: y.value, left: x.value, opacity: 0.5, background: active.value && 'red' })
// → "top:12px;left:40px;opacity:0.5"        (active is false here)
```

- **Numbers get `px`**, except the properties that are unitless by
  spec: `opacity`, `z-index`, `zoom`, `order`, `flex` (`-grow`,
  `-shrink`), `font-weight`, `line-height`, `scale`, `aspect-ratio`,
  `grid-row`/`column`/`area` (and their `-start`/`-end` forms),
  `column-count`, `columns`, `orphans`, `widows`, `tab-size`,
  `animation-iteration-count`, and `--custom` properties. The grid
  placements are in that list because of a real trap: the kanban probe
  caught `grid-column: 2px` before the list did.
- **`false`/`null`/`undefined` entries are skipped**, so conditionals
  compose without ternary noise: `background: active.value && 'red'`.
- **camelCase maps to kebab-case** (`marginLeft` → `margin-left`);
  `--custom-properties` pass through as written.

**`<style>` in a template is component-owned CSS.** The tag never enters
the DOM tree: the parser lifts it out and the engine injects its text as a
**constructed stylesheet** adopted by the document (`adoptedStyleSheets`)
**once per template**: mount a thousand instances, get one stylesheet.
The mechanics:

- It is a CSSOM stylesheet, not a `<style>` element, so it works under a
  strict [Content Security Policy](/docs/csp/) with no nonce. (Browsers
  without constructable stylesheets (pre-2023) fall back to a
  `data-lemonade` `<style>` element.) Either way it dedupes by call-site
  identity (the template literal itself).
- It **persists for the document lifetime**: unmounting every instance
  does not remove it, remounting never duplicates it. Styles are treated
  like code: loaded once, kept.
- The CSS must be **static**: `${...}` inside `<style>` fails `LJS-105`,
  because styles are parsed once. Per-instance values belong in
  `style="${...}"` or CSS custom properties.
- Styles are **global by design**; there is no scoping rewrite. The
  convention is the same one the Studio uses everywhere: prefix every
  selector with your component class (`lm-chip`, `lm-carousel-*`).
  Branch templates inject when the branch first renders.

The two halves meet in the **single-file component**: one `.ts` file
that carries its logic, its markup *and* its working styling, in every
deployment (by value, by name, or as a custom element) with no build
step and no companion artifact.

## In practice: the receipt

**The carousel is a single-file block.** It is the first Studio block
built after style hoisting landed, and its file layout is the claim:

- `src/index.ts` contains the component *and* its full stylesheet in a
  `<style>` tag, every selector prefixed `lm-carousel-*`.
- There is **no `style.css`** in the package, no `./style.css` exports
  entry in its `package.json`, and its demo page loads **no `<link>`**.
- Its `package.json` description states the deployment fact: styles
  ship inside the component, in every form: `import Carousel`,
  `<Carousel />` by name, or `createWebComponent(Carousel)` in a page
  that has never heard of a bundler.

```javascript
// components/carousel/src/index.ts (abridged)
return html`<div class="lm-carousel" ...>
    <style>
        .lm-carousel { position: relative; overflow: hidden; ... }
        .lm-carousel-strip { display: flex; ... }
        .lm-carousel-dragging .lm-carousel-strip { transition: none; }
    </style>
    ...
</div>`;
```

**`css()` carries the dynamic half.** The same carousel positions its
strip with a live computed transform; the modal builds drag positions
with `css({ top: y.value, left: x.value })`; the kanban places cards
with unitless `gridColumn` values, and the helper's unitless table is what
keeps that last one honest.

The engine suite pins all of it: hoisted styles stay out of the
component DOM, inject once across many instances, inject from branches
on first render, reject `${...}` with `LJS-105`. And `css()`'s unit,
camelCase, custom-property and conditional behavior is pinned case by
case (`tests/style.test.ts`).

## Reference

```javascript
import { css } from 'lemonadejs';

css(values)        // → "prop:val;prop:val", for style="${...}"
// numbers → px, except unitless props (opacity, z-index, zoom, order,
//   flex(-grow/-shrink), font-weight, line-height, scale, aspect-ratio,
//   grid-row/column/area(+-start/-end), column-count, columns, orphans,
//   widows, tab-size, animation-iteration-count, --custom)
// false/null/undefined entries skipped (conditionals compose)
// camelCase → kebab-case; '--custom': 'x' passes through

// Component-owned CSS
html`<div class="lm-name">
    <style>.lm-name { ... }</style>     <!-- lifted at parse, never in the tree -->
</div>`
```

- One injection per template, `data-lemonade`-tagged, persists for the
  document lifetime; remounts never duplicate.
- Static only: `${...}` inside `<style>` is `LJS-105`. Dynamic values
  go in `style="${() => css({...})}"` or CSS custom properties.
- Global by design: prefix selectors with the component class, the
  `lm-<name>-*` convention.

For attribute binding mechanics, read [Attributes](/docs/attributes/);
for the block-building conventions around `lm-*` classes, read
[Building blocks](/docs/building-blocks/).