---
title: Styling with StyleX
description: Use typed StyleX objects for component styling, plain document CSS for page chrome, and one stylesheet contract for pages, fragments, and streams.
order: 4
---

# Styling with StyleX

Your product card, checkout button, and low-stock warning should stay styled after full-page renders,
mutation fragments, and deferred streams. Kovo's default component styling path is `@kovojs/style`,
the Kovo-owned StyleX fork: components author typed style objects in TSX, the compiler extracts
deterministic atomic CSS, and rendered HTML keeps readable `kv-*` classes plus `data-style-src`
provenance for `kovo explain`.

Use plain document CSS for global page chrome, resets, fonts, and document-level theme tokens.
Because Kovo renders light DOM, tokens are ordinary CSS custom properties and theming does not cross
shadow boundaries. Use StyleX when a component owns the style; use plain CSS when the document owns
the surface.

## Style a component

Import the style package as `style`, define style groups near the component, and compose them through
the `style={...}` JSX prop:

```tsx
import * as style from '@kovojs/style';

const cardStyles = style.create({
  root: { padding: 16, color: style.tokens.sys.color.onSurface },
  lowStock: { color: style.tokens.customColor('warning').color },
});
```

Each property of `cardStyles` is an opaque `StyleHandle`, not a record of generated classes or CSS
rules. Pass handles directly to `style={...}` or `style.attrs(...)`; nested arrays and falsy
conditions compose without exposing compiler metadata. A literal, object spread, type cast, legacy
`$$css` record, or handle from a second installed copy of `@kovojs/style` fails the runtime
provenance check.

The public `style.attrs(...)` result contains ordinary `class` and optional inline `style`
attributes. Kovo may also emit `data-style-src` in rendered HTML for `kovo explain`, but that
provenance is build-owned output and is not a field applications read or construct.

## Seed themes

Use `defineTheme` when an app wants one seed color to drive the UI token system. The generated theme
returns concrete light/dark values plus deterministic CSS custom properties:

```ts
import { defineTheme } from '@kovojs/style';

export const theme = defineTheme({
  seed: '#6750A4',
  colors: {
    success: '#16a34a',
    warning: '#f59e0b',
  },
  shape: {
    cornerMedium: '0.625rem',
  },
});

export const themeCss = theme.css;
```

The CSS defines Material reference palette variables such as
`--kovo-theme-ref-palette-primary-40`, system role variables such as
`--kovo-theme-sys-color-primary`, and dark overrides under `:root[data-theme="dark"]`. Apps can
select a theme by setting document attributes or classes; Kovo does not add a runtime theme store.

When a theme needs precise control, keep it in the public seed form. Add named colors, choose a
scheme variant, tune contrast, and override shape tokens in the same `defineTheme({ seed, ... })`
call:

```ts
import { defineTheme } from '@kovojs/style';

export const theme = defineTheme({
  seed: '#6750A4',
  colors: {
    accent: { value: '#0ea5e9', blend: true },
  },
  contrast: 0.25,
  shape: {
    cornerSmall: '2px',
  },
  variant: 'vibrant',
});
```

`defineTheme({ seed, ... })` is the one app-facing theme constructor. `base`/`sys` theme derivation
and variable-override classes are internal maintenance mechanisms, not app-facing APIs.

## Run it

Build the app, then inspect the emitted stylesheet rather than trusting the TSX:

```sh
kovo build ./src/app.ts
find dist -name '*.css' -print
```

Open the emitted CSS file and confirm the component's `kv-*` atoms and theme variables are present.
That is the contract the page, fragment, and deferred-stream paths all share.

## Compose component states

Style objects can be selected with normal TypeScript conditionals:

```tsx
/** @jsxImportSource @kovojs/server */
import { component } from '@kovojs/core';
import * as style from '@kovojs/style';

const cardStyles = style.create({
  root: {
    backgroundColor: style.tokens.sys.color.surface,
    borderColor: style.tokens.sys.color.outlineVariant,
    borderStyle: 'solid',
    borderWidth: 1,
    color: style.tokens.sys.color.onSurface,
    padding: 16,
  },
  lowStock: {
    borderColor: style.tokens.customColor('warning').color,
    color: style.tokens.customColor('warning').color,
  },
});

export const ProductCard = component({
  render({ item }: { item: { id: string; stock: number } }) {
    return (
      <article kovo-key={item.id} style={[cardStyles.root, item.stock < 3 && cardStyles.lowStock]}>
        <h2>{item.id}</h2>
        <p>{item.stock} in stock</p>
      </article>
    );
  },
});
```

Two token families appear here. `style.tokens.sys.*` are the Material _system roles_ every theme defines —
`surface`, `onSurface`, `outlineVariant`, `primary`, and so on — derived from the seed so a theme
change re-skins them everywhere. `style.tokens.customColor('warning')` reads one of the _named extra colors_
you declared under `defineTheme({ colors: { warning } })`; it returns a group (`.color`, `.onColor`,
`.colorContainer`, …) for palette entries that aren't part of the system role set. Reach for `sys.*` for
ordinary surfaces and text, and `customColor(name)` for app-specific accents like a low-stock warning.

The compiler sees every referenced object and compiler-known token, extracts the CSS at build time,
and routes state/query-driven style toggles through the same attribute update plan as other Kovo
bindings. That means late mutation fragments and deferred chunks can only reference classes already
present in the app stylesheet.

Static `style.keyframes(...)` constants resolve to deterministic animation names. The extractor
binds each `animationName` reference to the literal emitted name and writes the matching
`@keyframes` block once into the CSS asset, deduped across every component that references it.

## Overrides

Public styled components expose typed `style` or `styles` override props. Put overrides last so the
caller wins by StyleX's property-level merge order:

```tsx
import * as style from '@kovojs/style';
import { Button } from '@kovojs/ui/button';

const toolbarStyles = style.create({
  saveButton: { minWidth: 112 },
});

export function Toolbar() {
  return <Button style={toolbarStyles.saveButton}>Save</Button>;
}
```

Use document CSS for page layout classes such as `.site-bar` or `.docs-shell`; use StyleX for
component-local styles and component override surfaces.

## Declare stylesheets for pages

Kovo owns the framework CSS contract. An emitted page lists its required stylesheet assets once, and
the same hints serve full-page renders, mutation fragments, and deferred fragments. The docs site
declares its stylesheet on the route:

```tsx
import { route, stylesheet } from '@kovojs/server';
import { siteThemeCss } from './theme.js';

export const siteStylesheets = [
  stylesheet('./styles.css', {
    href: '/assets/site.css',
    theme: siteThemeCss,
  }),
] as const;

export const cartPage = route('/cart', {
  meta: siteMeta,
  page: () => <CartPage />,
  stylesheets: siteStylesheets,
});
```

App-authored `style.create(...)` rules stay in TSX. `kovo build` collects those component atoms into
the emitted stylesheet, so app code does not export CSS strings or read private rule metadata by
hand.

## Declare stylesheets for fragments and streams

A mutation fragment can patch into a long-lived document that predates the fragment's styles or, with
split CSS, into a page that never loaded them. Declare stylesheets on the route/component metadata
that owns the generated live target. Enhanced success and failure fragments inherit those assets
from the generated renderer:

```ts
export const cartPage = route('/cart', {
  stylesheets: siteStylesheets,
  page: () => <CartPage />,
});
```

Deferred chunks do the same through the component and route metadata that own the late-rendered
region:

```tsx
import { component } from '@kovojs/core';
import { route } from '@kovojs/server';

export const ProductGrid = component({
  queries: { productGrid },
  render: ({ productGrid }: { productGrid: { items: unknown[] } }) => (
    <section>{productGrid.items.map(renderProduct)}</section>
  ),
});

export const cartPage = route('/cart', {
  page: () => <ProductGrid />,
  stylesheets: siteStylesheets,
});
```

The streamed chunk arrives as a fragment whose first child is its stylesheet link:

```html
<kovo-fragment target="product-grid"
  ><link rel="stylesheet" href="/assets/site.css" />
  <section kovo-c="product-grid" kovo-deps="product">...</section>
</kovo-fragment>
```

Stylesheet assets are deduped by `href` within each response, in page order, and a re-referenced
stylesheet resolves from the browser's HTTP cache. When fragment targets map to split stylesheets,
`stylesheetsForTargets(manifest, targets)` selects exactly the assets a fragment response needs from
a build manifest whose entries carry `fragmentTargets` metadata.

## Co-located raw CSS

Prefer StyleX for component styles. Raw co-located CSS remains an escape hatch for rules that need
plain CSS syntax; the compiler can scope those rules to the component host and preserve
fragment-target metadata so late fragments can request their styles. Scoping comes from the compiler
rather than shadow DOM, because shadow boundaries break IDREF wiring, form participation, and ARIA.

Practical summary:

- Component styling: typed `@kovojs/style` objects and theme tokens compiled to readable atomic CSS.
- Document styling: plain CSS for resets, fonts, page chrome, and generated CSS custom-property
  themes.
- Every render path — page, fragment, deferred chunk — declares its stylesheets; the framework
  dedupes and delivers.

## Next

- [Components & copy-in UI](/guides/components/) — styled `@kovojs/ui` components and their typed override surfaces.
- [Streaming & defer](/guides/streaming/) — the deferred streams these hints serve.
- [Mutations & forms](/guides/mutations/) — fragment responses end to end.

<details>
<summary>Spec & diagnostics</summary>

Style scoping, tokens, stylesheet hints, and late fragment delivery: SPEC §13.1. The
data-attribute / `[hidden]` binding posture and the update-plan grammar: SPEC §4.8. Why shadow DOM
was rejected: SPEC §3.1.

API reference: [@kovojs/core](/api/core/), [@kovojs/server](/api/server/), [@kovojs/style](/api/style/), [@kovojs/ui](/api/ui/).

</details>
