Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default defineConfig({
items: [
{ label: 'Getting Started', slug: 'guides/getting-started' },
{ label: 'Bring Your Own Renderer', slug: 'guides/renderers' },
{ label: 'Styling', slug: 'guides/styling' },
{ label: 'Attributes', slug: 'guides/attributes' },
{ label: 'Properties', slug: 'guides/properties' },
{ label: 'API', slug: 'guides/api' },
Expand Down
15 changes: 9 additions & 6 deletions docs/src/content/docs/guides/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,25 @@ import { Card, CardGrid } from '@astrojs/starlight/components';
<Card title="Bring Your Own Renderer" icon="puzzle" href="/guides/renderers/">
Use your preferred templating library — Lit, htm, or a custom renderer.
</Card>
<Card title="Attributes" icon="settings" href="/guides/attributes/">
<Card title="Styling" icon="pencil" href="/guides/styling/">
Style components with adopted style sheets, css helpers, and CSS modules.
</Card>
<Card title="Attributes" icon="setting" href="/guides/attributes/">
React to attribute changes on your custom elements.
</Card>
<Card title="Properties" icon="layers" href="/guides/properties/">
<Card title="Properties" icon="pen" href="/guides/properties/">
Manage reactive properties with type-safe accessors.
</Card>
<Card title="API" icon="code" href="/guides/api/">
<Card title="API" icon="document" href="/guides/api/">
Explore the full pion API reference.
</Card>
<Card title="Dispatching Events" icon="notification" href="/guides/events/">
<Card title="Dispatching Events" icon="link" href="/guides/events/">
Emit and listen to custom DOM events from hooks.
</Card>
<Card title="Virtual Components" icon="view-in-ar" href="/guides/virtual/">
<Card title="Virtual Components" icon="random" href="/guides/virtual/">
Compose lightweight components without custom element registration.
</Card>
<Card title="TypeScript" icon="terminal" href="/guides/typescript/">
<Card title="TypeScript" icon="forward-slash" href="/guides/typescript/">
Get full type inference for hooks, props, and events.
</Card>
</CardGrid>
183 changes: 183 additions & 0 deletions docs/src/content/docs/guides/styling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
---
title: Styling
description: Style pion components with adopted style sheets — the css tagged template, the styleSheets option, native CSS modules, and light DOM alternatives.
---

pion components render into a [shadow root](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) by default, so their styles are naturally isolated from the page. This guide covers the supported ways to apply styles: inline in templates, via adopted style sheets, with native CSS modules, and the options available for light DOM components.

## Inline styles in templates

The simplest approach is a `<style>` element inside your template:

```js
import { component, html } from '@pionjs/pion';

function App() {
return html`
<style>
:host {
display: block;
padding: 1rem;
}
</style>
<div class="content">Hello!</div>
`;
}

customElements.define('my-app', component(App));
```

This works fine for small components, but the browser parses the stylesheet once per element instance. For shared styles or components rendered many times, prefer adopted style sheets.

## Adopted style sheets

pion supports [constructable stylesheets](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM#applying_styles_inside_the_shadow_dom) through the `styleSheets` option. Sheets are constructed once and adopted by every instance of the component:

```js
import { component, html, css } from '@pionjs/pion';

const style = css`
:host {
display: block;
padding: 1rem;
}
.content {
color: var(--my-app-color, currentColor);
}
`;

function App() {
return html`<div class="content">Hello!</div>`;
}

customElements.define('my-app', component(App, { styleSheets: [style] }));
```

`styleSheets` accepts a mix of strings and `CSSStyleSheet` instances — strings are converted to stylesheets automatically.

:::tip[Recommended pattern]
Define styles in a dedicated file next to your component (e.g. `my-app.style.ts`) and import it. This keeps the component function focused on behavior and makes styles reusable:

```js
// my-app.style.ts
import { css } from '@pionjs/pion';

export const style = css`
:host { display: block; }
`;
```

```js
// my-app.ts
import { component, html } from '@pionjs/pion';
import { style } from './my-app.style';

customElements.define('my-app', component(App, { styleSheets: [style] }));
```
:::

### The `css` tagged template

`css` is a tagged template that returns a plain string. It supports interpolation, which makes it easy to compose styles or build themes from parts:

```js
import { css } from '@pionjs/pion';

const base = css`
:host { display: block; }
`;

const theme = css`
:host { --my-app-color: rebeccapurple; }
`;

export const style = css`
${base}
${theme}
.content { color: var(--my-app-color); }
`;
```

:::caution
Since `css` returns a string, interpolating a value like `0` or `false` results in an empty string (falsy values are dropped). Coerce interpolated values to strings when in doubt.
:::

### Attaching styles to the renderer function

Instead of the options object, you can set `styleSheets` as a static property on the component function itself:

```js
function App() {
return html`<div class="content">Hello!</div>`;
}

App.styleSheets = [style];

customElements.define('my-app', component(App));
```

If both are provided, the renderer's `styleSheets` take precedence over the ones passed to `component()`.

### The `sheet()` helper

If you need a `CSSStyleSheet` directly (for example to adopt it on a document or another shadow root), use the `sheet` helper:

```js
import { sheet } from '@pionjs/pion';

const styles = sheet(':host { display: block; }', '.content { color: red; }');

document.adoptedStyleSheets = [...document.adoptedStyleSheets, styles];
```

## Native CSS modules

With the [CSS modules](https://developer.chrome.com/docs/css-ui/css-modules) import attribute (`with { type: 'css' }`), the browser hands you a ready-made `CSSStyleSheet`. Since `styleSheets` accepts existing instances, they work out of the box:

```js
import { component, html } from '@pionjs/pion';
import styles from './my-app.css' with { type: 'css' };

function App() {
return html`<div class="content">Hello!</div>`;
}

customElements.define('my-app', component(App, { styleSheets: [styles] }));
```

Bundler notes:

- **Vite** and **Rollup** support `with { type: 'css' }` — the imported value is a constructed stylesheet that adopts cleanly into shadow roots.
- This syntax requires a modern browser or a bundler that transforms it; it does **not** work from a plain `<script type="module">` without a build step.

Styles inside a CSS module are still global within the sheet (they are not scoped per class name like Sass-style CSS modules) — you get a `CSSStyleSheet`, not transformed class names. Isolation comes from the shadow root, exactly like the other approaches.

## Light DOM components

When you create a component with `useShadowDOM: false`, there is no shadow root to adopt stylesheets into — the `styleSheets` option is ignored, and `<style>` tags inside your template would leak into the page. Light DOM components should rely on document-level styles:

```js
import { component, html, sheet } from '@pionjs/pion';

const styles = sheet('.my-app { color: rebeccapurple; }');

document.adoptedStyleSheets = [...document.adoptedStyleSheets, styles];

function App() {
return html`<div class="my-app">Hello!</div>`;
}

customElements.define('my-app', component(App, { useShadowDOM: false }));
```

Alternatively, keep your styles in a shared `css` template and add it to the page's adopted stylesheets from a single module.

## API

| API | Signature | Description |
|---|---|---|
| `css` | ``css`...` `` | Tagged template returning a string; supports interpolation. Alias of `tagged`. |
| `sheet` | `sheet(...styles: string[]): CSSStyleSheet` | Constructs a single `CSSStyleSheet` from one or more strings. |
| `styleSheets` (option) | `(CSSStyleSheet \| string)[]` | Stylesheets adopted on the component's shadow root. |
| `shadowRootInit` (option) | `ShadowRootInit` | Passed to `attachShadow()`; defaults to `{ mode: 'open' }`. Useful for e.g. `delegatesFocus: true`. |
| `useShadowDOM` (option) | `boolean` | Set to `false` to render to light DOM — note that `styleSheets` is then ignored. |
18 changes: 9 additions & 9 deletions docs/src/content/docs/hooks/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,31 @@ description: Browse every pion hook — useState, useEffect, useReducer, useCont
import { Card, CardGrid } from '@astrojs/starlight/components';

<CardGrid>
<Card title="useCallback" icon="repeat" href="/hooks/usecallback/">
<Card title="useCallback" icon="link" href="/hooks/usecallback/">
Memoize a callback function to avoid unnecessary re-renders.
</Card>
<Card title="useContext" icon="share" href="/hooks/usecontext/">
<Card title="useContext" icon="external" href="/hooks/usecontext/">
Access shared context from parent components.
</Card>
<Card title="useEffect" icon="sync" href="/hooks/useeffect/">
<Card title="useEffect" icon="analytics" href="/hooks/useeffect/">
Run side effects in response to state or prop changes.
</Card>
<Card title="useHost" icon="settings" href="/hooks/usehost/">
<Card title="useHost" icon="setting" href="/hooks/usehost/">
Access the host custom element instance.
</Card>
<Card title="useLayoutEffect" icon="speed" href="/hooks/uselayouteffect/">
<Card title="useLayoutEffect" icon="clock" href="/hooks/uselayouteffect/">
Run side effects synchronously before the browser paints.
</Card>
<Card title="useMemo" icon="memory" href="/hooks/usememo/">
<Card title="useMemo" icon="database" href="/hooks/usememo/">
Cache expensive computations between renders.
</Card>
<Card title="useProperty" icon="layers" href="/hooks/useproperty/">
<Card title="useProperty" icon="pen" href="/hooks/useproperty/">
Create reactive properties with optional attribute reflection.
</Card>
<Card title="useReducer" icon="filter-list" href="/hooks/usereducer/">
<Card title="useReducer" icon="list-format" href="/hooks/usereducer/">
Manage complex state with a reducer function.
</Card>
<Card title="useRef" icon="bookmark" href="/hooks/useref/">
<Card title="useRef" icon="magnifier" href="/hooks/useref/">
Store a mutable value that persists across renders.
</Card>
<Card title="useState" icon="star" href="/hooks/usestate/">
Expand Down
4 changes: 3 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ type Renderer = (element: Element) => TemplateResult;
interface Options {
baseElement: HTMLElement;
observedAttributes: string[];
useShadowDOM: boolean
useShadowDOM: boolean;
shadowRootInit: ShadowRootInit;
styleSheets: (CSSStyleSheet | string)[];
}

declare function component(
Expand Down