Three third-party Solid packages, declared in solid-rs.toml, installed by
solid-rs install, and driven from Rust:
| package | what it does here |
|---|---|
@solidjs/router |
the route tree, the nav, and /hello/:name read back with useParams |
@kobalte/core |
a Progress bar, a Button, a Checkbox and Tabs — behaviour and ARIA, no styles |
@solid-primitives/timer |
createIntervalCounter, a signal that counts on its own schedule |
Nothing this page does is solid-rs's behaviour. What solid-rs contributes is that Rust could express it.
solid-rs install imports every installed package and writes a ready-to-paste
extern block per subpath into bindings/. That is not a convenience — it is
the difference between working and not:
-
The Solid 2.0 router has no
<Router>or<Route>. It is config-first:createRouter({ routes })returns an instance that is the provider component. There is no<A>either; the router installs one document-level click handler and claims everya[href]. -
@kobalte/corepublishes only a wildcard subpath export ("./*"), so there is nothing at the package root to read. The generator expands the wildcard against the filesystem and finds all 28 components. -
A headless library publishes state, not styles.
Progress.Fillsets--kb-progress-fill-widthinline and sets nowidthat all — the fill could as easily be a height, a transform or a clip path, so choosing is the app's job. Miss the rule that consumes it and the fill is a block element inside the track, i.e. a bar that reads as 100% full at every value:.bar { width: var(--kb-progress-fill-width); }
The same shape as
.tab[data-selected]: select on what the library exposes. -
A Kobalte export is the root component, with the parts hung off it.
Progress.Trackexists;Progress.Rootdoes not. The generated file says so:// `<Progress>` is the root; its parts are `<Progress.Fill>`, `<Progress.Label>`, // `<Progress.Track>`, `<Progress.ValueLabel>` fn Progress();
bindings/ is reference material, not compiled — solid-rs parses src/ only.
Binding a package's whole surface would collide with names the app and Solid
already own (@solidjs/router alone exports action and query), so you copy
what you need. Names that cannot be imported are kept in the file as comments
saying why, rather than silently dropped.
Regenerate any time with solid-rs bindings.
Three things, added for it:
- Dotted component tags.
<Tabs.List>is a member expression on the imported value, so only the head —Tabs— is imported. This is the entire authoring style of Kobalte and libraries shaped like it. on*on a component is an ordinary prop. On an element it is a DOM handler and solid-rs requires a zero-arg closure; on a component the component decides what it is called with. Kobalte'sonChange={set_agreed}receives the new state. The element rule applied to components rejected every callback prop in the ecosystem.- A bare event handler need not be the last attribute. A bare closure has no
delimiters, so where it ends and the next attribute begins is decided by
parsing the longest prefix that is a valid expression. Reading greedily to the
tag's
>used to swallow everything written after a handler.
A module-level const used as a JSX tag already worked, which is what makes
<Routes> possible.
Client routes are URLs the server has never heard of. /hello/solid-rs is a
real page when you navigate to it and a missing file the moment you press
refresh — so solid-rs preview and solid-rs dev serve index.html for any
path with nothing behind it, and the router takes it from there.
The fallback is conditional, because doing it unconditionally is the classic
way to break a site: a page fetching a missing script would get HTML back, and
the browser would report a syntax error at <!doctype rather than a missing
file. So it applies only to requests that are actually navigations — a readable
method, a path that does not name an asset extension, and an Accept that asks
for text/html. Both halves are needed: a script tag sends */*, and an asset
URL typed into the address bar sends text/html.
The catch-all route is last in the tree, because matching is in order:
RouteDef { path: "/*path", component: NotFound },The router splits the path on /*, so what follows the star names the parameter
that captures the remainder — params.path here. A bare /* matches equally
well but captures nothing, which leaves the page unable to say what was missing.
The status is 200, and that is the honest answer. A client-rendered server
cannot tell a route it does not know about from a route that does not exist, and
only one of those is an error. Serving a 404.html here instead — the
convention static hosts use — was tried and is actively wrong: this branch is
reached by every client route, so /hello/solid-rs would answer 404 with an
error page and the app would never boot.
A real 404 status needs the routing to happen on the server, which is what
mode = "ssr" is for: there the app matches the URL itself and sets the status
with http_status(404). SSR mode already answers deep links, since every
document request goes through the render rather than through the filesystem.
- Router hooks need the provider.
useLocationanduseParamsread a context the instance provides, so the whole layout lives inside the instance's render-prop child. A nav rendered as a sibling of<Routes>throwsContextNotFoundErrorthe moment it asks which page it is on. It still stays mounted across navigations — onlyprops.childrenis swapped. - Links must be real paths.
href="#/about"is a fragment; the router resolves it against the current page and navigates nowhere. This is what made an earlier version of this example look like a dead page.
# from the repo root
cargo build -p solid-rs
cd examples/deps
solid-rs install # installs the three packages, then writes bindings/
solid-rs build
solid-rs preview # http://localhost:3000install passes --legacy-peer-deps: the RC ecosystem pins peer ranges
inconsistently (@kobalte/core pins @solidjs/web to an exact rc the toolchain
has moved past), and npm's strict resolution would refuse an install that is
fine at run time. Note that no second copy of Solid is installed — solid-js
resolves out of the toolchain through esbuild's nodePaths, so the bundle
contains exactly one reactive runtime.
node e2e/render.mjs # the page, and navigation
node e2e/deeplink.mjs # refresh on a deep link, and the catch-alle2e/render.mjs is 22 checks that assert the libraries' behaviour rather than that markup
reached the page: the progress bar's role="progressbar" and aria-valuenow,
the data-checked attribute the CSS selects on, the roving tab selection, and
navigation driven by dispatching a real click on an anchor so the router's own
handler is what runs.
Two things the suite has to accommodate, neither a solid-rs concern:
- jsdom implements no layout, so
scrollIntoViewandgetBoundingClientRectare shimmed — Kobalte calls both while managing focus. - Two clicks inside one flush both read the pre-flush
count()and both write1. That is Solid's batching being correct, so the test lets each settle, as a user would.
e2e/deeplink.mjs covers the refresh case, and covers it on both sides: it
starts a real solid-rs preview and checks over HTTP that deep links serve the
document while missing assets stay 404, then boots the app in jsdom at
/hello/solid-rs — the first URL it ever sees, as a refresh would — and checks
the route matched on the very first render.