How spaday compares to alternatives

This page is for someone choosing how to put a Python-backed UI in front of users. It compares spaday with the other ways to do that and says when each one is the better pick. Read How spaday works first; this page repeats only what the comparison needs.

Where does the code run?

Every Python UI tool has to answer one question: when a user clicks, where does the code that responds run? Most of the differences between these tools come from that answer. There are three common answers.

In Python, on the server. The browser reports the event, a Python process handles it, and the browser shows the result. Streamlit, Gradio, Dash, Panel, Shiny, Solara, NiceGUI, and Reflex all work this way. They differ in how much of the page gets recomputed per event and in how the server keeps per-user state, but every interaction is a round trip.

In JavaScript you wrote. A frontend built with React, Vue, or Svelte talks to Python over an API. Python runs only when the frontend asks it to.

In the browser, from a description Python wrote. This is spaday’s answer. Python builds the component tree and attaches behavior as data: actions such as Toggle and SetProp, and bindings that keep props in step with a state store. The browser runtime interprets those. Python is called only when an action says so, through a model edit (SendPatch) or an endpoint call (CallEndpoint).

The answer decides how many users one server can hold, what the app can do while the network is slow, how much JavaScript the team writes, and where the components come from. The sections below go through each family.

Script re-run: Streamlit and Gradio

Streamlit runs your script from the top on every interaction and keeps a Python session per browser tab. It is the fastest way to turn an analysis into a page other people can use, and the model is easy to hold in your head: the page is whatever the script printed last. The cost is that every interaction goes to the server. A checkbox that hides a panel is a round trip and a partial re-run, and the server holds a session for every open tab. Custom widgets are React components in an iframe, with their own build.

Gradio is organized around functions instead of scripts. Declare the inputs and outputs, and Gradio builds the form and calls the function on submit. It is the usual way to demo a model, and it is good at that.

In spaday, hiding a panel is a client-side action and Python is not involved. The trade cuts both ways: spaday has no “write Python and it appears” path. Logic that needs Python is an explicit endpoint call or model edit, and the rest of the behavior is written as data. For a throwaway script that exists to show one analysis, Streamlit or Gradio gets a page up faster, and that is the case to use them for.

Callbacks: Dash, NiceGUI, and Reflex

Dash is the closest of the server-side tools to spaday in shape. The layout is a tree declared in Python, and behavior is separate from it: callbacks are Python functions with declared inputs and outputs, and each one runs over HTTP when an input changes. The server is stateless by design, which is why Dash scales out more easily than the session-per-tab tools. For logic that must not round trip, Dash has clientside callbacks, which are JavaScript source strings evaluated in the page.

spaday differs in both halves. Dash’s tree is built from Dash components, which are React components wrapped for Python, so a new component means a React build. spaday’s tree is built from any web-component library that publishes a Custom Elements Manifest, with no wrapper code. Dash’s default for behavior is a Python function; spaday’s default is a client-side action. spaday’s escape hatch is a handler registered by name, never an evaluated string, and that is what makes a tree safe to send to clients you do not trust.

NiceGUI puts Vue and Quasar components behind Python event handlers that run over a socket, with each client’s state kept in the Python process. Reflex compiles a Python description into a Next.js app; state and event handlers are Python classes on the backend, and events travel over a WebSocket. Both are pleasant to write. Both keep the Python process in the loop for every event, and Reflex adds a Node build to deployment.

Server-side reactivity: Panel, Shiny, and Solara

Panel, Shiny for Python, and Solara give you a reactive graph in Python. Widgets are inputs to it, dependent values recompute when inputs change, and a Python process per session holds the graph. When the interesting work is in Python, this is a strong model: you write the analysis as a reactive graph and the UI comes with it. Panel runs on the Bokeh server and also offers JavaScript links between widgets for cases that should not round trip. Shiny’s reactivity is per session and fine-grained. Solara writes React-style components in Python on top of ipywidgets.

spaday’s reactive engine lives in the browser and is narrower by design: it evaluates field bindings and computed expressions over a signal store rather than arbitrary Python. That narrowness is what makes it shippable as data and lets a two-way control or a derived value update with no server. These tools and spaday also combine. spaday’s widget is an anywidget, so a spaday tree can sit inside a Panel, Solara, Shiny, or Marimo app when one of those is the host you want.

Notebook widgets: ipywidgets and Voilà

ipywidgets sync widget state over the Jupyter comm, and Voilà turns a notebook into an app by running a kernel per viewer. spaday’s notebook host is the same engine as its web host with a different wire. A tree developed in a notebook is served as a web app unchanged, and the web app needs no kernel per viewer.

A Python server with a JavaScript frontend

A FastAPI or Django API with a frontend written in React, Vue, or Svelte is the most flexible option and the one most web teams reach for. Everything the browser can do is available, the frontend scales as far as the API does, and the component ecosystem is all of npm. The costs are a second codebase, a build pipeline, an API contract between the two, and the fact that people who only write Python cannot change the UI.

spaday keeps this architecture (client-side logic, static assets, a plain API) and lets you write it from Python. The components are real web components, the assets are static files, and any Python server can serve the tree. The difference is where JavaScript goes. In a frontend you write it inline, anywhere. In spaday the tree carries a fixed action vocabulary, and JavaScript lives in handlers registered on the page and called by name with NamedJs, or in a wrapper around an imperative library. You can still write any JavaScript you need; it is referenced from the tree instead of embedded in it, which is what keeps the tree serializable. If you already have a JavaScript frontend, spaday does not replace it. The fragment rung of the integration ladder puts a spaday tree in one node of a page you own.

Server-rendered HTML: templates and htmx

Django or Jinja templates, with htmx or Turbo for partial updates, render HTML on the server and swap fragments into the page. This is a good fit for CRUD apps with modest interactivity, and it needs no client framework at all. Each interaction is still a request, client-side state is whatever the DOM holds, and the components are the HTML you write. spaday differs on all three counts. The price is a wasm runtime in the page, about a megabyte for the core and another for transports before compression, which is the size of an ordinary React bundle and an order of magnitude under a Pyodide download.

Python in the browser: PyScript, Pyodide, Shinylive, and Panel’s convert

PyScript, Shinylive, and panel convert take a different route to getting the server out of the interaction loop: they run Python itself in the browser through Pyodide. That works, at the cost of a multi-megabyte download and several seconds of startup before the first render.

spaday runs in Pyodide as well; the standalone examples are built that way. The difference is that it does not depend on it. Because the tree and its behavior are data, the browser needs only the wasm runtime. The Python that produced the tree can be on a server, in a notebook kernel, in Pyodide, or already exited: once the tree is written out, Python is not needed to run it.

The Quansight criteria, with spaday added

In 2022 Quansight published Dash, Voilà, Panel, and Streamlit: our thoughts on the big four dashboarding tools. It is still the most useful short comparison of those four, and it uses more criteria than the one question above: where the tool came from, Jupyter integration, scalability, multi-page apps, ease of use, authentication, big data, and styling. Its conclusions hold up. Voilà is the simplest and starts a kernel per visitor. Streamlit has the tightest feedback loop and does not scale. Dash keeps state in the client and scales, but needs web knowledge for anything advanced. Panel is the one for full applications in pure Python. This section runs spaday through the same eight criteria so the two can be read together.

Where it came from. The four were built to publish data science work: a notebook, a script, or a Plotly figure, put in front of colleagues. spaday was built for applications with many concurrent users and long-lived pages, such as dashboards and control panels, where a server round trip per interaction is the thing to avoid. That origin explains most of what follows.

Jupyter. Voilà and Panel are notebook-native, Streamlit and Dash are not. spaday’s widget is an anywidget, so a tree renders in a cell with its state synced over the comm, and the same tree is served as a web page without change. The tutorial is a notebook for that reason. This is closer to Panel than to Dash, with one difference: the web app does not need a kernel or a Python session per viewer to run what the notebook ran.

Scalability. Quansight’s ranking comes down to where state lives: Dash keeps it in the client, Panel and Streamlit keep a session per user, Voilà keeps a kernel per user. spaday keeps UI state in the client too, in the browser’s signal store. The server holds only a transports model per session, or a shared model in a multi-tenant Hub, or nothing at all when the page is a static tree plus REST calls. Assets are static files. That puts spaday with Dash on this criterion, with one addition: toggles, bindings, and routing never reach the server either.

Multi-page applications. The post found this hard in Streamlit and Voilà, possible in Dash with web knowledge, and covered in Panel by its pipelines. spaday has two layers. A Switch bound to a URL parameter routes inside one page on the client, with history entries and back/forward, and Lazy fetches a branch the first time it is shown. For a conventional site, mount_site takes a mapping of paths to pages and mounts one set of assets for all of them, on Starlette, FastAPI, aiohttp, Flask, or Tornado. See Serve and embed.

Ease of use. Getting started is as easy as with any of the four: build a tree, call serve, and the standalone examples show most of the basics in a few lines each. The components are typed Python classes generated from each library’s manifest, so an editor completes props and a bad prop value fails in Python before anything is served. There is a learning curve after that, and it is in proportion to what becomes reachable: an action vocabulary, bindings against a store, several models on one page, routing, and the escape hatches. A Streamlit script has less to learn because it has less to reach. In shape spaday is closest to Dash: declare a layout, then attach behavior, except the behavior is client-side data instead of Python callbacks.

Authentication. None of the four ships it in the open-source version, and neither does spaday. The routes spaday needs are ordinary routes on the host framework, so a FastAPI dependency or Starlette middleware guards them the same way it guards any other endpoint. A login page can be served with its tree inline, so the only route an anonymous user can reach is one HTML page.

Big data. The post gives this to Panel for its Dask and Datashader integration, and that is a real advantage when the work is a server-side computation whose result is a picture or an aggregate. spaday takes a different route to the same problem and holds its own on it. The UI state never carries the data. Data moves as incremental, revisioned patches over transports, so a table of 100,000 rows is sent once and then receives only the rows that changed, and spaday-regular-table renders only the cells in the viewport. spaday-perspective goes further: rows stream as Arrow into a browser-side engine that computes views, group-bys, and aggregations locally, or the browser holds only the visible slice and every scroll and sort is answered by the server engine. In both cases the interaction is fully virtualized, the browser holds only what it shows, and spaday syncs the few kilobytes of layout and configuration around it. spaday does not bundle a compute engine of its own; it connects to the one you already run, whether that is Perspective, a database, or a Dask cluster behind an endpoint.

Customization and styling. Dash needs CSS, Streamlit offers little, Panel gained templates. spaday’s answer is the design systems it binds to, eight of them at the time of writing, each with its own components and theming. Colors and tokens are set from Python through css(), dark and light page modes follow one root class, and a package’s bundle or a single element can be swapped for your own. See Ship your own variant.

Criterion

Dash

Voilà

Panel

Streamlit

spaday

Built for

standalone dashboards

publishing notebooks

full data applications

scripts to tools

multi-tenant applications

Jupyter

no

native

native

no

anywidget; same tree as the web app

Scales

yes, client state

kernel per user

session per user

session per tab

yes, client state; small server model

Multi-page

with web knowledge

no

pipelines

awkward

client routing and mount_site

Ease of use

moderate

easiest

moderate

easiest

easy start; curve scales with reach

Authentication

enterprise

host

host

host

host framework

Big data

enterprise

kernel

Dask, Datashader

memory-bound

patches over transports; virtualized tables

Styling

CSS

notebook themes

templates

limited

design-system packages, Python tokens

Side by side

Tool

Interaction logic runs

Server state per user

Components

Hosts

Streamlit

Python, script re-run

session per tab

Streamlit widgets, iframe customs

web

Gradio

Python function per submit

little

Gradio components

web

Dash

Python callbacks over HTTP; optional JS strings

none

React wrappers

web

NiceGUI

Python handlers over a socket

per client

Vue and Quasar

web

Reflex

Python handlers over a WebSocket

per client

React, Next.js build

web

Panel

Python reactive graph; optional JS links

session

Bokeh models, ipywidgets

web, notebook, Pyodide

Shiny

Python reactive graph

session

Shiny components

web, Shinylive

Solara

Python components on ipywidgets

session

ipywidgets

web, notebook

Voilà

Python kernel

kernel per viewer

ipywidgets

web

JS frontend + Python API

your JavaScript

as designed

npm

web

Templates + htmx

Python per request

as designed

your HTML

web

spaday

browser-interpreted actions and bindings; Python by explicit call

a transports model, or none

any web-component library with a manifest

web, embedded, notebook, Pyodide

Trade-offs

Interactions that need Python: on par. Exploratory analysis and per-click model inference are the clearest cases. In Streamlit, Panel, or Shiny the handler is a Python function. In spaday it is the same function behind an endpoint, called with CallEndpoint, or a model edit sent with SendPatch that the server answers. The Python runs in the same place and costs the same. What differs is everything around it: the rest of the page keeps working without a round trip, and the server does not need a session to make that call possible.

Custom JavaScript: any you need, by reference. The action vocabulary is small so that a tree stays data that is safe to serialize and ship. When a page needs more, NamedJs calls a handler registered on the page by name, and a wrapper puts an imperative library behind a component with typed props. Both keep the tree serializable. Both are about as much JavaScript as a Dash clientside callback or a Panel link, written once and referenced from Python instead of pasted inline.

An existing frontend: embed spaday in it. The integration ladder runs from serve down to a fragment, which renders a spaday tree in one node of a page you own, and the assets can be mounted under a prefix of your app. A team with a React or Vue frontend adds a Python-authored region without giving anything up, and two libraries on one page can share one engine.

Multi-tenancy: ahead. This is where the families separate most. Voilà starts a kernel per viewer. Streamlit keeps a session per tab. Panel, Shiny, and Solara keep a session per user with the reactive graph inside it. Each user costs an interpreter or a slice of one, and that caps how many users one server can hold. spaday was built for the opposite case. Interaction logic runs in the browser, the server keeps a small transports model per session, and a Hub routes each connection to its own tenant session while sharing models across tenants where they should be shared. A page can mirror several models at once under separate namespaces, so a global model, a per-tenant model, and a per-tab form can all sit on one page with no glue code. A server’s cost per user is a model, not a process, and the same tree serves one viewer or a thousand. See Sync a UI to a server.

Age: behind. Streamlit and Dash have had years of components, hosting options, and answered questions. spaday and its component packages are newer, and its ecosystem is the web-component libraries it binds to rather than a catalog of its own. That is a bet that web-component libraries outlast any one Python catalog, and the bet is too young to judge.