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, noconnectedCallback. - 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=Trueand 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:
examples/counter.py— counter with step controlsexamples/waveform_editor.py— audio waveform editor using Peaks.jsexamples/codemirror/editor.py— CodeMirror 6 with theme/language switching and complex import maps
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
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)
| File | Size | Uploaded | |
|---|---|---|---|
| starelements-0.1.3.tar.gz | 111.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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