Prototype scoped scripts and DOM-bound signals for Astro components by matthewp · Pull Request #18433 · withastro/astro · GitHub
Skip to content

Prototype scoped scripts and DOM-bound signals for Astro components - #18433

Draft
matthewp wants to merge 2 commits into
mainfrom
mp/astro-ui-scoped-scripts
Draft

matthewp wants to merge 2 commits into
mainfrom
mp/astro-ui-scoped-scripts

Conversation

@matthewp

@matthewp matthewp commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Prototype for discussion, not intended to merge as-is. The package is private and the API is a sketch.

Changes

  • Adds @astrojs/ui, an integration that lets components use plain TypeScript with signals bound to the DOM. It's progressive enhancement: the server renders HTML, and <script scoped> attaches to it. The aim is a small, framework-free model that's easy to write correctly, including for agents.
  • <script scoped> runs once per component instance. Its scope(selector) sees only that instance's markup: not sibling instances, not child component internals, but it does see content passed into a child's <slot>. It throws when nothing matches. The integration parses .astro source with @astrojs/compiler-rs before compilation, wraps each instance and slot in comment markers, and turns the scoped script into a normal bundled script that registers with a small client runtime (~3.6 KB gzipped, including alien-signals).
  • signal() in frontmatter renders its value with a marker in {count} and value={count}. The client revives each signal once by id, so one signal passed to several components stays shared in the browser, with no script needed. value and checked bindings are two-way.
  • scope.list(container, template, row => item) adopts server-rendered rows and clones a <template> for new ones. Reordering and filtering use keyed reconciliation ported from zebra's List.
---
import { signal } from '@astrojs/ui';
const count = signal(Astro.props.start ?? 0);
---
<output>{count}</output>
<button class="inc">+</button>

<script scoped>
  const count = scope('output').text();
  scope('.inc').on('click', () => count(count() + 1));
</script>
  • Adds a 7GUIs Cells example (test/fixtures/cells): a 26 × 100 spreadsheet where each cell is a formula signal plus a computed value, with a formula engine shared by server and client so the first render has real values. About 6 KB gzipped of client JS.

Open questions (instances added after load, row disposal, build-time selector checks) are listed in the package README.

Testing

  • Unit tests for the source transform, the runtime's instance/slot ownership, signal revival and list adoption (using linkedom), and list reconciliation.
  • Playwright tests for Cells: server-rendered values, edits propagating through dependent cells, cycle detection and recovery, error values, and formula bar and keyboard editing.
  • Playwright tests against a demo fixture: per-instance counters, a card that must not see slotted todo checkboxes, a signal shared between two script-less components, and adding/filtering/sorting/removing todos.

Docs

  • No docs update, because this is an unpublished prototype.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

@github-actions github-actions Bot added the pkg: integration Related to any renderer integration (scope) label Oct 17, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg: integration Related to any renderer integration (scope)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant