Skip to main content

starelements

Custom web components, defined in Python, powered by Datastar signals.

You decorate a Python function, and it becomes a custom element. Each instance gets its own scoped signals — no JavaScript class boilerplate, no state collision between multiple instances on the same page.

Why?

starelements gives you encapsulated custom elements where signals are scoped per instance and JS dependencies are declared with the component, not in your app headers. Define it once, app.register() it, and use it like any HTML element.

Features

  • Decorator = component — one @element("tag-name") and your function is a custom element. No class inheritance, no connectedCallback.
  • Scoped signals — each component instance gets its own signal namespace. Two <my-counter> on the same page won't step on each other.
  • ESM imports built in — pull in third-party JS via imports={"chart": "https://esm.sh/chart.js@4"}. No bundler config needed.
  • Light DOM by default — your component's markup lives in the real DOM, so <form> submission, CSS selectors, and accessibility tools all just work. Shadow DOM is opt-in.
  • Skeleton loading — set height="400px", skeleton=True and users see a shimmer placeholder until the component initializes. Prevents layout shift.

Installation

Requires Python 3.12+ and StarHTML.

pip install starelements

Quick Start

A complete counter app — two instances with different initial values, each tracking its own state:

from starhtml import Div, Button, Span, star_app, serve
from starelements import element, Local

@element("my-counter")
def Counter():
    return Div(
        (count := Local("count", 0)),
        (step := Local("step", 1)),
        Button("-", data_on_click=count.set(count - step)),
        Span(data_text=count),
        Button("+", data_on_click=count.set(count + step)),
    )

app, rt = star_app()
app.register(Counter)

@rt("/")
def home():
    return Div(Counter(count=10, step=5), Counter(count=0))

if __name__ == "__main__":
    serve()

Attributes you pass (count=10, step=5) become signal values inside that instance.

Local objects are signal references — count + step isn't evaluated in Python. It builds a JS expression, so count.set(count + step) produces $$count = ($$count + $$step) for the browser.

Examples

Skeleton loading

The skeleton option shows a shimmer placeholder while the component initializes, preventing layout shift:

@element("heavy-chart", height="400px", skeleton=True,
         imports={"chart": "https://esm.sh/chart.js@4"})
def HeavyChart():
    return Div(
        Script('''
            new chart.Chart(refs('canvas'), {type: 'bar', data: {...}});
        '''),
        Canvas(data_ref="canvas", style="width:100%;height:100%;"),
    )

Setup and cleanup with Script()

Script() inside your render tree runs once when the component connects. Use onCleanup() to tear down resources when the element is removed:

@element("video-player")
def VideoPlayer():
    return Div(
        (playing := Local("playing", False)),
        Video(data_ref="video", src="/video.mp4"),
        Button("Play/Pause", data_on_click="$$playing = !$$playing"),
        Script('''
            const video = refs('video');
            effect(() => $$playing ? video.play() : video.pause());
            onCleanup(() => video.pause());
        '''),
    )

Inside Script(), imported modules are available by alias, signals are accessible as $$name, and refs('name') returns elements marked with data_ref.

For more complex examples, see:

API Reference

@element decorator

Parameter Type Default Description
name str required Custom element tag (must contain a hyphen, e.g. my-counter)
shadow bool False Use Shadow DOM instead of Light DOM
form_associated bool False Reserved for future form association support
height str | None None Shorthand for min-height; skeleton defaults to True when set
width str "100%" Width dimension
dimensions dict | None None Full dimension dict (overrides height/width)
skeleton bool | None None Show shimmer placeholder while loading
imports dict | None None ESM imports — {alias: specifier}
import_map dict | None None Additional import map entries
scripts dict | None None UMD scripts — {globalName: url}
events list | None None Custom events the component emits

Registration

app.register() mounts static file routes and adds the component's CSS, import map, JS runtime, and templates to the app-wide headers (included on every page):

app, rt = star_app()
app.register(Counter)              # single component
app.register(Counter, DatePicker)  # multiple at once

CLI

starelements includes a CLI for bundling npm packages into ESM bundles using esbuild:

starelements bundle     # bundles packages listed in pyproject.toml [tool.starelements]

Configure packages in your pyproject.toml:

[tool.starelements]
bundle = ["chart.js@4", "@codemirror/state@6.4.1"]

Development

uv sync --all-extras          # install dev + test dependencies
uv run scripts/build.py  # build JS runtime from TypeScript
uv run ruff check src/ tests/   # lint
uv run pytest tests/ -v          # run tests

The TypeScript runtime source lives in typescript/. The build script compiles it to src/starelements/static/starelements.min.js.

License

Apache 2.0

Metadata

Release files for starelements 0.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for starelements 0.1.3
File Size Uploaded
starelements-0.1.3.tar.gz 111.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for starelements 0.1.3
File Interpreter ABI Platform
starelements-0.1.3-py3-none-any.whl Python 3 none any Details

Total release size: 138.1 kB

Release files / starelements-0.1.3.tar.gz

Download URL starelements-0.1.3.tar.gz
Size 111.3 kB
Tags Source
SHA-256 checksum
How to use checksums
b2ebe30c369411eab15c83af31bf03267d822bf4e9e1e70bbf4a6105e3ddd7db
BLAKE2b-256 checksum
How to use checksums
3ccdd904e9b7172f5b66541e799565c64cd51137693a646a84e23344be55c447
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Feb 20, 2026.

Transparency log

Release files / starelements-0.1.3-py3-none-any.whl

Download URL starelements-0.1.3-py3-none-any.whl
Size 26.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4608f8e26d9a4fa905642ae543b800bd14cd8c15de3e7586c1ca286b3d8dadde
BLAKE2b-256 checksum
How to use checksums
5cd5e50225af088391dfd2b8e1ac01dea4cefb8adf1a27f7bcf228f4488d39b6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Feb 20, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.3 This release

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page