⚡ Velo
Beautiful, reactive Python web apps in minutes, by drag & drop or in code. Zero dependencies.
Velo lets anyone build a polished web app (dashboards, forms, data tools, chat UIs) two ways:
- No code:
velo studioopens a drag & drop builder. Drop components on a canvas, wire buttons to actions, compute values with spreadsheet-style formulas, and watch the real app update live beside it. - Python: write a page function with
ui.input(...),ui.chart(...)and friends.
The two meet in the middle: any Studio app exports to clean Python in one click, and Python developers can plug their own functions into Studio buttons. It runs on your laptop and deploys anywhere with one command, all in pure-stdlib Python.
pip install velo-ui
velo studio # drag & drop builder → http://localhost:7860
import velo
app = velo.App("Hello", accent="#6366f1")
@app.page("/")
def home(ui):
name = ui.input("Your name", placeholder="Ada")
n = ui.slider("How excited?", 1, 10, 7)
ui.title(f"Hello {name or 'world'}" + " 🎉" * n)
app.run()
python app.py # or: velo run app.py
🎨 Velo Studio: build apps without code
velo studio # edits ./app.velo.json (created if missing)
velo studio sales.velo.json -t dashboard # start from a template
velo run app.velo.json # run it like any Velo app (hot reload included)
velo export app.velo.json # eject to plain Python: app.py
What you get
| Drag & drop canvas | 38 components in 6 groups: text, inputs, buttons, tables, charts, chat, columns, cards, tabs, expanders, sidebar. Drop inside containers, reorder, duplicate. Click to add works too (touch friendly). |
| Inspector | Edit every property. Inputs are variables: {name} shows one in text, =price * qty computes a value, just like a spreadsheet. |
| Actions without code | Buttons run steps top to bottom: set a variable, add to a list, flip a switch, reset inputs, show a message, go to a page, or call a Python function, each with an optional only if condition. |
| Show if | Any component can appear only when a formula is true (agree, len(entries) > 0). |
| Live preview | The real app runs next to the canvas (Split view) and hot-swaps on every edit without losing what you typed. Desktop and mobile frames. |
| Code view & export | See the exact Python your app equals, copy it, or download app.py. It's ordinary Velo code. |
| Safety nets | Autosave, unlimited undo/redo, validation (unknown variables, typos in formulas, duplicate names) with one-click jump to the problem, and rename-refactor: rename a variable and every {reference} follows. |
| Templates | Hello world, Dashboard (multi-page, filters, KPIs), Form + table with CSV export, Data explorer (upload a CSV, pick columns), Chatbot, Loan calculator. |
Formulas, in one minute. They're Python expressions run in a safe sandbox (no imports, no file or network access):
Hello {name}! text with a variable
Total: {price * qty:,.2f} compute + format → 1,234.50
=count + 1 a whole computed value
=csv(file) rows from an uploaded CSV
=where(rows, 'city', city) filter rows · =group(rows, 'month', 'sales') group & sum
=sum(col(rows, 'sales')) totals · mean() min() max() len() round() sorted()
='Pro' if score > 90 else 'Free' conditions · list comprehensions work too
Mix in Python whenever you want. Set Hooks file to hooks.py in App & Data, then:
# hooks.py — plain Python, any library you like
def send_welcome(ctx): # a button action: "Run Python hook → send_welcome"
ctx.toast(f"Email sent to {ctx['email']} ✉️", "success")
ctx.set("sent", True)
def ask_ai(ctx, message): # a chat component's hook: return the reply
return my_llm(message, history=ctx["chat"])
Or load a Studio app from Python and keep going:
import velo
app = velo.load("app.velo.json")
@app.action("send_welcome") # same as a hooks.py function
def send_welcome(ctx): ...
@app.page("/admin") # hand-written pages live next to Studio pages
def admin(ui): ui.title("Admin")
app.run()
The .velo.json file is plain, diff-friendly JSON, so it belongs in git and code review like any source file. Full reference: docs/STUDIO.md.
Why Velo?
| Velo | Typical script-rerun frameworks | |
|---|---|---|
| Install size | 0 dependencies, ~140 KB | Dozens of transitive packages |
| What re-runs on a click | Only the page function | Often the whole script |
| UI updates | DOM morphing: no flicker, focus and caret kept | Partial or full re-paint |
| Hot reload | Keeps your state (widgets and ui.state survive a save) |
Session usually resets |
| Syntax error while editing | Overlay in the browser; app keeps running | Varies |
| Tables | Built-in search, sort and paging on the client | Varies |
| Charts | Built-in SVG line, area, bar and donut with tooltips, no deps | Usually needs a plotting library |
| Typing in inputs | Debounced; sliders throttled | Often one round trip per change |
| Live push from Python | app.push() from any thread (SSE) |
Limited |
| Deploy | velo docker (vendors itself), honours $PORT |
Extra setup |
| Theme | Light and dark with a toggle; one accent colour themes everything | Config files |
| No-code builder | velo studio: drag & drop, live preview, exports to Python |
Code only |
Install
pip install velo-ui
velo new myapp && cd myapp && velo run app.py
Needs Python 3.9+. Nothing else.
The 60-second tour
Pages and navigation
Two or more pages give you an automatic sidebar nav with client-side routing.
@app.page("/", title="Dashboard", icon="📊")
def dashboard(ui): ...
@app.page("/settings", title="Settings", icon="⚙️")
def settings(ui): ...
Widgets return values
name = ui.input("Name", placeholder="…") # also: type="password" | "email"
bio = ui.textarea("Bio", rows=4)
age = ui.number("Age", 30, min=0, max=120)
level = ui.slider("Level", 0, 100, 50, suffix="%")
city = ui.select("City", ["Karachi", "Lahore"]) # returns the object, any type
plan = ui.radio("Plan", ["Free", "Pro", "Team"]) # segmented control
tags = ui.multiselect("Tags", ["a", "b", "c"]) # chips
ok = ui.checkbox("I agree"); dark = ui.toggle("Dark mode")
day = ui.date("Start"); col = ui.color("Brand", "#6366f1")
f = ui.upload("CSV file", accept=".csv") # UploadedFile(.name .data .text())
if ui.button("Save", icon="💾"): ... # True on the click run
ui.download("Export", data, "report.csv")
State and callbacks
@app.page("/")
def counter(ui):
ui.init(count=0) # defaults, set once per browser session
def add(step):
ui.state.count += step
with ui.row():
ui.button("−", on_click=add, args=(-1,), kind="secondary")
ui.button("+", on_click=add, args=(1,))
ui.metric("Count", ui.state.count)
Callbacks run before the re-render, so the page always shows consistent state. Use ui.get("key") and ui.set("key", v) to read or write any widget that has a key=.
Layout
a, b, c = ui.columns(3) # or ui.columns([2, 1])
a.metric("Revenue", "$12.4k", "+8%") # containers proxy every component…
with b.card("Settings", "Optional subtitle"): # …or use `with`
ui.toggle("Enabled")
t1, t2 = ui.tabs(["Chart", "Data"]) # tabs switch instantly on the client
with ui.expander("Advanced"): ...
with ui.sidebar: ui.select("Region", [...])
with ui.row(): ... # horizontal button group
Data and charts (pandas optional)
ui.table(df) # DataFrame, list of dicts, dict of lists…
ui.line_chart(df, x="date", y=["sales", "cost"])
ui.area_chart(rows, x="day", y="revenue", title="Revenue")
ui.bar_chart(rows, x="day", y=["a", "b"], stacked=True)
ui.pie_chart({"k": ["A", "B"], "v": [60, 40]})
ui.metric("Users", 1520, delta=-12, inverse=True) # inverse: down is good
Text and feedback
ui.title, ui.header, ui.subheader, ui.text, ui.caption, ui.markdown (safe), ui.code, ui.json, ui.badge, ui.link, ui.image (URL, path, bytes or PIL), ui.progress, ui.info, ui.success, ui.warning, ui.error, ui.toast, ui.divider, ui.html (trusted).
Chat apps
ui.init(msgs=[])
if q := ui.chat_input("Ask me…"):
ui.state.msgs += [("user", q), ("assistant", my_llm(q))]
for role, text in ui.state.msgs:
ui.chat_message(role, text)
Speed tools
@velo.cache(ttl=300) # shared across sessions, TTL + LRU
def load_data(path): ...
ui.autorefresh(5) # live dashboards: re-run every 5 s while visible
app.push() # from a background thread: refresh all open browsers
app.push(ui.session_id) # …or just one
ui.rerun(); ui.navigate("/other")
Deploy
velo run app.py --prod # 0.0.0.0, no reload, no tracebacks, gzip on
velo docker app.py # writes Dockerfile and vendors velo (works for app.velo.json too)
docker build -t myapp . && docker run -p 8000:8000 myapp
Velo reads $PORT and $VELO_HOST, so the same app runs unchanged on Render, Railway, Fly.io, Cloud Run, Koyeb, Heroku or a VPS. /health is there for load balancers. Behind nginx, turn off buffering for /api/stream (Velo already sends X-Accel-Buffering: no).
CLI
| Command | What it does |
|---|---|
velo studio [app.velo.json] [-t TEMPLATE] [--port N] |
Open the drag & drop builder |
velo run app.py or velo run app.velo.json [--port N] [--host H] [--open] [--prod] [--no-reload] |
Run an app |
velo export app.velo.json [-o app.py] |
Turn a Studio app into plain Python |
velo new NAME |
Scaffold a starter app |
velo docker [app.py] [--port N] |
Generate a Dockerfile and vendor Velo |
velo version |
Print the version |
How it works
- The browser opens one SSE stream (used for hot reload and server push) and sends small JSON events (
set,click,submit) to/api/render. - The server applies the event to that browser's session, runs callbacks, then re-runs only the page function. That builds a compact component tree.
- The client morphs the live DOM to match the tree. Unchanged nodes aren't touched, so focus, caret, scroll position, open tabs and table sort all survive.
- When you save a file, the reloader re-executes your module, swaps the page functions into the running app and tells browsers to re-render. Sessions stay where they were.
Customising the look
Pass App(accent="#10b981", theme="dark", wide=True, icon="🚀"), or override any CSS variable (--accent, --radius, --font…) with ui.html("<style>:root{--r:6px}</style>").
Current limits
- Sessions live in memory: use one process per app, or sticky sessions if you scale out.
- Uploads travel as base64 JSON (practical up to roughly 25 MB).
- No streaming of partial output inside a single run yet. For long jobs, use a background thread with
app.push().
Testing
python -m pytest -q # or: python tests/test_velo.py && python tests/test_studio.py
Contributing
Contributions are very welcome. Start with CONTRIBUTING.md, pick a good first issue, and say hi in Discussions. Please follow the Code of Conduct.
MIT licensed. © 2026 Muhammad Rafay Shaikh
Metadata
Release files for velo-ui 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| velo_ui-0.2.0.tar.gz | 104.5 kB | With provenance | Details for velo_ui-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| velo_ui-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for velo_ui-0.2.0-py3-none-any.whl |
Total release size: 203.6 kB
Release files / velo_ui-0.2.0.tar.gz
| Download URL | velo_ui-0.2.0.tar.gz |
|---|---|
| Size | 104.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bad9ed85cc714041c4896365656b1faa7a62a4ea63e8a54115ad5da2f2c5dab6
|
|
BLAKE2b-256 checksum How to use checksums |
3d2a80798045a52d2c7b0c0f6c4868db18eae10eeeca0c3bb20bd129fe091553
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 9, 2026.
Transparency logRelease files / velo_ui-0.2.0-py3-none-any.whl
| Download URL | velo_ui-0.2.0-py3-none-any.whl |
|---|---|
| Size | 99.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
06f3fbb0c0d95c26ab773e1ebe2d07a0979fb4004f90cabdf4d8f75c7cc2dfda
|
|
BLAKE2b-256 checksum How to use checksums |
784556db9497a29eddf6aac50e0cf321cd63bbe8b070cbb76cd2d124737df579
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 9, 2026.
Transparency logSecurity for velo-ui 0.2.0
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.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for velo_ui-0.2.0.tar.gz
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 Nov 9, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.