pindoba
A multi-framework component system for Astro, React, Svelte, and Vue.
This is an alpha, and the current focus is deliberate: functionality and architecture— writing a component once and running it natively in four frameworks, without rewriting the logic for each one.
The first and most important focus: build the full component set across every framework, with parity and tests, so we can prove the ideas actually work before we refine them.
With the components working, raise the bar on how they look and feel — a deliberate visual and interaction polish across the system.
Make sure everything holds up in real applications — not just in isolation — so the ideas are validated against actual usage.
Every milestone, what it covers and where it stands — the long version of this strip.
A Panda sva() recipe per component. One slot per rendered element, no framework anywhere near it.
connect*() runs the logic, reads the recipe and returns one ready-to-spread prop bag per slot. This is the part that is written once.
Four thin templates that render the tree and spread the bags. A normalizer adapts naming at the boundary — class → className, and so on.
Behaviour, accessibility wiring and styling live in the two framework-agnostic layers. What each framework keeps is what it is actually good at: Svelte gets runes and bind:this, React gets hooks and refs, Astro gets slots, Vue gets its own reactivity — none of them re-implement the component.
// packages/core/button/src/connect-button.ts
export function connectButton(options) {
const classes = buttonStyles.raw({ emphasis, size });
return defineConnect({
slots: { root: { ...rest, class: css(classes.root) } },
passThrough,
});
}
- 01
Reads the recipe.
buttonStyles.raw()resolves the variant props into the class list for every slot. - 02
Merges passThrough. A consumer's per-slot styles and attributes are folded into the same bag, so templates never merge anything themselves.
- 03
Returns one bag per slot.
rootPropscarries the classes, ARIA, data attributes and handlers — everything the element needs, ready to spread.
<script>
const api = $derived(connectButton(rest));
</script>
<button bind:this={element} {...api.rootProps}>
{@render children?.()}
</button>
const api = connectButton({ ...rest, class: className });
<button {...reactProps(api.rootProps)}>{children}</button>
---
const api = connectButton({ ...rest, class: className });
---
<button {...api.rootProps}><slot /></button>
Adding an attribute or fixing an ARIA string is a one-file change in core — every framework picks it up. Read how it is being rolled out on the roadmap.
Same props, same slots, same accessibility wiring in all four — only the import line and the template syntax change. The variants above (emphasis, size,feedback) come from one recipe, and the behaviour from one connectButton().
import Button from "@pindoba/astro-button";
<Button emphasis="primary">Create</Button>
To do
In progress
Done
Every color, space, and type value now flows from one source of truth — tweak a token and it ripples through every framework.
A filled header works as a hero strip — it carries its own palette while the body stays in the card's neutral surface.
A feedback-tinted footer doubles as a confirmation strip — success here, but warning and danger read the same way.
hana opened #47 — reduce input layout shift
ci build failed on main — lint step
ci deploy to staging succeeded
alex commented on #42
mel merged #44 — tab group focus order
- Pick a milestone before saving.