This is the full developer documentation for Mercury
# Easily share your discoveries
> Mercury helps you share discoveries with non-technical users, from simple insights to life-changing breakthroughs.
## What is Mercury?
[Section titled “What is Mercury?”](#what-is-mercury)
Mercury is an open-source framework that makes it **simple to share Python notebooks with non-technical users**.\
You write your analysis in a notebook; Mercury turns it into a **clean, responsive web app** with interactive widgets and no frontend code.
Our goal is to help people - especially in **science, research, and data-heavy work** - share their results easily ❤️
***
## Why Mercury?
[Section titled “Why Mercury?”](#why-mercury)
No frontend required
Stay in Python and your notebook. Mercury takes care of the web layout, navigation, and styling automatically.
Rich interactive widgets
Build interactive apps with a wide library of widgets. When a widget value changes, cells below it are automatically recomputed — no callbacks to maintain.
Designed for non-technical users
Share results with colleagues who do not code. They get a simple, friendly web interface; you keep full control in Python.
Clean layout by default
Mercury gives your notebook a nice, consistent layout out of the box. You focus on the analysis, not on CSS or HTML.
Easy deployment
Run your app in Docker or deploy to our cloud with no complex setup. From notebook to shared app in minutes.
Live preview while you work
See a live preview of your app directly in **MLJAR Studio** or **JupyterLab** (via our extension). Adjust widgets and layout and instantly see the result.
## How does Mercury work?
[Section titled “How does Mercury work?”](#how-does-mercury-work)
1. **Create a notebook in Python**\
Use your usual environment: JupyterLab or MLJAR Studio.
2. **Add Mercury widgets**\
Insert widgets in your notebook cells. They are interactive and automatically trigger re-execution of cells below them.
3. **Preview your app live**\
Use the built-in extension to see how your notebook looks as a web app while you edit.
4. **Deploy with Docker or cloud**\
Package your notebook with Docker or use our cloud deployment. No complicated server setup needed.
## How Mercury can be used?
[Section titled “How Mercury can be used?”](#how-mercury-can-be-used)
### Research & Science
Turn your analysis into a simple, interactive application that your team or lab can use — no coding required.
### Data Science & Analysis
Share dashboards with business users. Let them adjust parameters, run scenarios, and download results easily through a friendly web UI.
### Education & Training
Create interactive teaching materials and exercises in notebooks, then present them as easy-to-use web apps.
## Start here
[Section titled “Start here”](#start-here)
[Start the quickstart ](/get-started-with-mercury/)Install Mercury, run your first notebook app, and see the full workflow in a few minutes.
[Explore widgets ](/widgets/overview/)Learn about the widget library and how to make your notebooks interactive without writing callbacks.
[Live preview in MLJAR Studio ](/guides/mljar-studio-preview/)See how to work with Mercury apps directly inside MLJAR Studio, with live preview.
[Deploy with Docker or cloud ](/deployment/overview/)Discover how to share your notebook as a web app using Docker or our cloud – with almost no setup.
# Mercury Deployment
> How to deploy Python notebooks as Mercury web applications
Mercury allows you to turn Python notebooks into fully interactive web applications and deploy them with very little effort.\
Depending on your needs, you can choose between **self-hosting** or using a **managed cloud service**.
This section of the documentation explains the available deployment options and helps you decide which one best fits your use case.
***
[Docker ](/deploy/dockerfile/)Run Mercury as a self-hosted web application using Docker containers. Ideal for local environments, on-premise servers, and custom infrastructure.
[Cloud ](/deploy/cloud/)Deploy Mercury notebooks using the managed MLJAR Cloud service. The easiest way to publish apps without managing servers or infrastructure.
### Which option should you choose?
[Section titled “Which option should you choose?”](#which-option-should-you-choose)
* **Docker deployment** is recommended if you want full control over the environment, need to run Mercury on your own infrastructure, or prefer a reproducible setup for development and production.
* **Cloud deployment** is the simplest option if you want to focus on your notebooks and users, without dealing with servers, Docker images, or configuration.
Both approaches allow you to deploy the same Mercury notebooks — only the hosting method differs.
### Need help with deployment?
[Section titled “Need help with deployment?”](#need-help-with-deployment)
If you have questions, encounter issues, or something does not work as expected, please **create an issue** in the Mercury repository.\
We actively monitor issues and are happy to help.
If you need help with **more advanced setups** — for example:
* deploying Mercury inside your company infrastructure,
* running it behind authentication or a proxy,
* integrating it with existing systems,
* or preparing a production-ready environment,
we also offer **paid support and consulting**.
Tip
We are friendly people, we know Mercury very well, and we genuinely enjoy helping users deploy it successfully.\
If you need hands-on help, just reach out - we will take care of the technical part 🙂
# Cloud Deployment
> How to deploy Python notebooks as Mercury web applications using the Mercury Cloud service
Prefer the terminal? Run `mercury publish` to sign in, configure a website, and choose files to upload. See [Publish from the terminal](/deploy/publish/) for the guided CLI and repeat updates.
Deploying Mercury apps in the cloud is the **simplest and fastest** way to share your notebooks as web applications.\
The Mercury Cloud service takes care of servers, configuration, and infrastructure, so you can focus entirely on your notebooks and your users.
Below is a step-by-step guide that walks you through the entire process.
***
1. **Create an account**
Start by creating an account at\
[platform.mljar.com](https://platform.mljar.com).
This account will be used to manage your Mercury applications, deployments, and secrets in one place.
2. **Open the Apps section**
After logging in, navigate to the **Apps** section in the dashboard.
Apps are used to host Mercury applications and control how they are deployed and exposed to users.
3. **Create a new site**
Click **Create new site** and choose a name for your application.\
This site will represent a single Mercury web app.
4. **Upload your files**
Upload all files required by your application. This typically includes:
* Jupyter notebooks (`.ipynb`) with Mercury apps
* Data files such as `.csv` or `.xls`
* A `requirements.txt` file with additional Python dependencies
* A `config.toml` file with Mercury configuration (optional)
Once uploaded, Mercury Cloud will automatically prepare the environment for your app.
5. **Define secrets (optional)**
In the **Apps** view, you can define **secrets** for your application.
Secrets are a safe place to store sensitive information such as:
* database connection strings,
* API keys,
* access tokens,
* or credentials required by external services.
These values are never exposed in your notebooks or source files. Notebooks access them as environment variables.
***
That’s it!\
After completing these steps, your Mercury application will be built and deployed in the cloud, ready to be shared with others.
If something is unclear or you need help, do not worry - we are happy to assist 🙂
# Docker Deployment
> How to deploy Python notebooks as Mercury web applications using Docker containers
This guide explains how to run **Mercury** as a web application using **Docker**.\
Docker allows you to package Mercury together with Python and all dependencies, making deployment predictable and reproducible across different machines.
## Create a Dockerfile
[Section titled “Create a Dockerfile”](#create-a-dockerfile)
Start by creating a file named `Dockerfile` in your project directory.
The following Dockerfile uses a lightweight Python image, installs Mercury, and starts the Mercury server on port `8888`.
Dockerfile
```docker
FROM python:3.12-slim
# Install Mercury
RUN pip install mercury==3.0.0a2
# You can also install needed packages here,
# For example install pandas
# RUN pip install pandas
# Directory where notebooks will be mounted
WORKDIR /workspace
# Mercury default port
EXPOSE 8888
# Start Mercury server
CMD ["mercury", "--ip=0.0.0.0", "--no-browser", "--allow-root"]
```
Workspace directory
The `/workspace` directory is important.\
Your local notebooks directory will be mounted there so Mercury can discover and serve your notebooks. In this setup, `/workspace` becomes Mercury’s default base directory for notebooks and `config.toml`.
## Docker commands
[Section titled “Docker commands”](#docker-commands)
Below are the most common Docker commands you will need when working with the Mercury container.
### Build the Docker image
[Section titled “Build the Docker image”](#build-the-docker-image)
Build the Docker image from the `Dockerfile` and tag it as `mercury-server`:
```bash
docker build -t mercury-server .
```
This command only needs to be run again if you change the Dockerfile code.
### Run the container (foreground mode)
[Section titled “Run the container (foreground mode)”](#run-the-container-foreground-mode)
This mode is useful during development. The container runs in the foreground and is automatically removed when you stop it.
```bash
docker run --rm -it \
-p 8888:8888 \
-v /my/directory/with/notebooks:/workspace \
mercury-server
```
After starting, open your browser and navigate to:
```plaintext
http://localhost:8888
```
Mount notebooks directory
The `-v` option mounts your local notebooks directory `/my/directory/with/notebooks` into the container.\
Because the container `WORKDIR` is `/workspace`, Mercury uses that mounted directory as its default base unless you explicitly pass `--working-dir`.
### Run the container (background mode)
[Section titled “Run the container (background mode)”](#run-the-container-background-mode)
For longer-running sessions or server-like usage, run Mercury in detached mode by adding `-d` flag:
```bash
docker run -d --name mercury-server \
-p 8888:8888 \
-v /my/directory/with/notebooks:/workspace \
mercury-server
```
The container will continue running in the background until you stop it explicitly.
### View container logs
[Section titled “View container logs”](#view-container-logs)
To inspect Mercury logs or verify that the server started correctly:
```bash
docker logs -f mercury-server
```
Press `Ctrl` + `C` to stop following the logs.
### Open a shell inside the container (debugging)
[Section titled “Open a shell inside the container (debugging)”](#open-a-shell-inside-the-container-debugging)
If you need to inspect the container environment or verify mounted files:
```bash
docker exec -it mercury-server /bin/sh
```
For example, you can list mounted notebooks:
```bash
ls -la /workspace
```
### Stop the container
[Section titled “Stop the container”](#stop-the-container)
If Mercury is running in background mode, stop it with:
```bash
docker stop mercury-server
```
### Stop and remove the container
[Section titled “Stop and remove the container”](#stop-and-remove-the-container)
To stop and remove the container in a single command:
```bash
docker rm -f mercury-server
```
# Publish from the terminal
> Sign in, create a website, and upload Mercury notebooks with mercury publish
Run this command in the directory containing your notebooks:
```bash
mercury publish
```
The guided CLI opens browser sign-in, asks for a website title and subdomain, and lets you select files with an interactive checkbox picker. Use arrow keys to navigate, **Space** to select, and **Enter** to continue. Review the destination, file list, and total size, then confirm publishing.
This command does not start a local Mercury server or execute your notebooks. Authentication and website management use [MLJAR Platform](https://platform.mljar.com). New websites are **public**, and the command displays their full address before creating them. The default hosting domain is `ismvp.org`.
## Files and dependencies
[Section titled “Files and dependencies”](#files-and-dependencies)
Select the notebooks and supporting files your app needs: Python helpers, data, images, models, `requirements.txt`, `runtime.txt`, and `config.toml`. At least one notebook must be selected. Notebooks and the three configuration files are preselected on the first publish; other files require explicit selection.
Hidden files/directories, common virtual environments, caches, build directories, symlinks, and common credential/key files are excluded from the picker. This is not a secret scanner: review your notebooks (including saved outputs), data, and configuration before uploading. Configure secrets on the platform instead of putting credentials in uploaded files.
Relative file paths are sent unchanged, including paths such as `data/sales.csv`. The platform must support these paths; a rejected upload reports the failing filename rather than silently flattening directories.
Include a `requirements.txt` listing the Python dependencies needed by your app. The CLI warns if it is not selected; it does not generate one from your local environment. For example:
```text
mercury
pandas
matplotlib
```
## Updating a website
[Section titled “Updating a website”](#updating-a-website)
After website creation, Mercury records the destination and selection in `.mercury-publish.json` in the project directory. After all uploads succeed, it also records file sizes, SHA-256 hashes, and the successful upload timestamp. No authentication tokens are saved in this file; sign in again on each publish.
Run `mercury publish` again to update the same website. The CLI verifies that your account can access its saved site ID, reuses your file selection, shows new, changed, and unchanged files, and asks for confirmation. You can change the file selection on every run. All selected files are uploaded, including unchanged ones.
Matching remote filenames are overwritten. Missing or deselected local files **do not delete remote files**. If the saved website is missing or inaccessible, the command stops rather than creating a replacement.
Uploads are per file, not an atomic release. A failure may leave some files updated remotely. The saved site ID and pending selection allow the next publish to retry the same destination; the previous successful upload record is retained.
“Upload complete” means the platform accepted the files, not that the app has finished dependency installation or restart. Check the app and platform dashboard after publishing. The CLI does not poll deployment readiness.
Keep the state file locally to retain the update destination. It can be excluded from version control using your project’s `.gitignore`. Treat it as deployment configuration and review changes before publishing from an unfamiliar checkout.
## Options
[Section titled “Options”](#options)
```bash
mercury publish --working-dir ./my-app
mercury publish --no-browser
mercury publish --login-timeout 600
mercury publish --help
```
* `--working-dir`: project directory; defaults to the current directory.
* `--no-browser`: print the sign-in link without opening a browser.
* `--login-timeout`: seconds allowed for browser sign-in; defaults to 300.
Publishing requires an interactive terminal. **Ctrl+C** cancels it without removing previously uploaded remote files. A project lock prevents two concurrent publishes; after a hard crash, remove `.mercury-publish.lock` only after checking that the other publishing process has stopped.
For alternate platform installations, `MLJAR_PLATFORM_BASE_URL` selects the HTTPS platform origin for a new deployment and `MLJAR_PLATFORM_DEFAULT_DOMAIN` selects its hosting domain. Existing deployments keep the saved platform origin.
# Getting Started
> Mercury framework documentation
Welcome! 👋\
This is the starting point for Mercury documentation. Pick a topic below to get going.
[Installation ](/docs/install/)Install Mercury Package.
[Quick Start ](/docs/quickstart/)Create your first web app with Mercury.
[Deploy ](/deploy/)Deploy web app and share with others.
[Examples ](/examples/)Inspiration and examples.
[Authentication ](/docs/authentication/)Protect your apps with a password.
[Arguments ](/docs/arguments/)Common server flags and options.
[Customization ](/docs/customization/)Configure app title, footer, and welcome message.
# Arguments
> Common Mercury server arguments
## Publish
[Section titled “Publish”](#publish)
To deploy an app instead of starting the server, use `mercury publish`. See [Publish from the terminal](/deploy/publish/) for sign-in, file selection, and deployment options.
## Log level
[Section titled “Log level”](#log-level)
Would you like to see more logs from `mercury`?\
Please use `--log-level=INFO` or `--log-level=DEBUG`. The default log level is `CRITICAL`.
## Session sharing
[Section titled “Session sharing”](#session-sharing)
Use `--keep-session` to give all viewers of an app one shared Python session. Widget changes and resulting outputs are synchronized across connected browsers. Mercury runs one notebook rerun at a time; changes received while it is busy are coalesced into the next rerun, starting at the earliest affected cell.
Without this option, each viewer gets independent widget values and kernel state.
## Timeout
[Section titled “Timeout”](#timeout)
Would you like to limit your server resources with usage timeout?\
Please set `--timeout=600`. The timeout value is in seconds.
## Working directory
[Section titled “Working directory”](#working-directory)
By default, Mercury uses the current directory as the base for notebooks and related files. To override it, use `--working-dir=/path/to/notebooks`.
When provided, Mercury will use that directory as the base for:
* notebook discovery,
* relative notebook paths passed on startup,
* `config.toml` lookup,
* relative file reads from inside notebooks.
# Authentication
> Restrict access to Mercury apps with a password
You can restrict access to your web apps by setting password when starting server:
```bash
mercury --pass=your-secret-here
```
Before opening the notebook user needs to provide the password:

If you want to have user-based authentication in your Mercury. It is paid option. Please reach us for more details contact - at - mljar.com
# Chat
> How to build chat-style interfaces with the Chat container in a Mercury App
The **Chat** widget is a container for displaying a sequence of `Message` widgets. It provides a simple API for building **chat-style interfaces** and ensures that new messages are **automatically scrolled into view**.
`Chat` does not render messages itself — each message is responsible for its own content and layout. The role of `Chat` is to **manage message order, visibility, and scrolling behavior**.
## Live Demo
[Section titled “Live Demo”](#live-demo)
You can try the `Chat` widget directly in this interactive example:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/chat.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/echo-chat?no-navbar)
## Basic Usage
[Section titled “Basic Usage”](#basic-usage)
Create a chat container and add messages to it:
```python
import mercury as mr
chat = mr.Chat()
chat.add(mr.Message(markdown="Hello 👋", role="assistant"))
chat.add(mr.Message(markdown="Hi!", role="user"))
```
Messages are displayed in the order they are added.
## Chat + Message Together
[Section titled “Chat + Message Together”](#chat--message-together)
`Chat` is designed to work together with the `Message` widget.
Each `Message`:
* renders its own content (Markdown, HTML, plots, tables, etc.)
* controls avatar, emoji, and formatting
`Chat`:
* stacks messages vertically
* shows a placeholder when empty
* scrolls automatically when new messages appear
## Auto-Scrolling Behavior ⭐
[Section titled “Auto-Scrolling Behavior ⭐”](#auto-scrolling-behavior)
One of the key features of `Chat` is **automatic scrolling**.
Whenever a new message is added:
* the chat scrolls to the **most recent message**
* scrolling waits briefly to allow large outputs (plots, images) to finish rendering
* a fixed-height chat stops following new content when the reader scrolls upward
Auto-follow resumes when the reader returns near the bottom of the fixed-height chat.
This makes `Chat` suitable for:
* conversational UIs
* LLM chat apps
* data exploration assistants
* streaming outputs
Note
By default, `Chat` does not create its own scrollbar. Scrolling happens in the surrounding Mercury application container. Set `height` when you want the chat itself to scroll.
## Fixed Height
[Section titled “Fixed Height”](#fixed-height)
Use `height` to create a fixed-height chat area with internal vertical scrolling. This is useful in column layouts where one side of the app contains a chat.
```python
import mercury as mr
left, right = mr.Columns([0.4, 0.6])
with left:
chat = mr.Chat(height="600px")
with right:
_ = mr.Markdown("Controls or analysis output")
```
The value accepts CSS height strings, for example `"600px"`, `"70vh"`, or `"calc(100vh - 120px)"`.
## Displaying Rich Outputs in Chat
[Section titled “Displaying Rich Outputs in Chat”](#displaying-rich-outputs-in-chat)
Because `Message` is a generic output container, `Chat` can display **any output supported by Mercury**.
### Chat with Charts
[Section titled “Chat with Charts”](#chat-with-charts)
```python
import matplotlib.pyplot as plt
import mercury as mr
chat = mr.Chat()
msg = mr.Message(markdown="Here is the chart:")
chat.add(msg)
with msg:
plt.plot([1, 2, 3], [2, 1, 4])
plt.title("Example plot")
plt.show()
```
The plot is rendered **inside the chat message**, and the chat scrolls automatically.
### Chat with DataFrames
[Section titled “Chat with DataFrames”](#chat-with-dataframes)
```python
import pandas as pd
import mercury as mr
chat = mr.Chat()
df = pd.DataFrame({
"product": ["A", "B", "C"],
"sales": [120, 90, 150]
})
msg = mr.Message(markdown="Sales data:")
chat.add(msg)
with msg:
display(df)
```
This pattern is ideal for **conversational data analysis**.
## Streaming Responses
[Section titled “Streaming Responses”](#streaming-responses)
A common pattern is to add an empty message first, then update it incrementally:
```python
msg = mr.Message()
chat.add(msg)
msg.append_markdown("Thinking")
msg.append_markdown("...")
msg.append_markdown("\n\nAnswer ready!")
```
The chat scrolls as content grows, making it ideal for **LLM streaming responses**. Streaming scroll updates are debounced, so many small chunks are coalesced into fewer scroll operations.
## Chat Props
[Section titled “Chat Props”](#chat-props)
### placeholder
[Section titled “placeholder”](#placeholder)
**type:** `string`
Text displayed when the chat contains no messages.
Default:
```text
💬 No messages yet. Start the conversation!
```
***
### scroll\_container\_selector
[Section titled “scroll\_container\_selector”](#scroll_container_selector)
**type:** `string`
CSS selector used to locate the preferred scrollable container.
Default:
```text
#mercury-main-panel, .mercury-main-panel
```
This is usually the main content area of a Mercury App.
Note
You usually do not need to change this value unless you are embedding Chat inside a custom layout.
***
### height
[Section titled “height”](#height)
**type:** `string`
CSS height for the chat message container.
Default: `""`
If empty, Chat keeps its natural content height and the surrounding app/page container scrolls. If provided, Chat uses that fixed height and enables internal vertical scrolling. A fixed-height chat always owns its scrollbar, even before its content is tall enough to overflow, so multiple chats cannot compete for the page scroll position.
Examples:
```python
mr.Chat(height="600px")
mr.Chat(height="70vh")
mr.Chat(height="calc(100vh - 120px)")
```
***
### scroll\_debounce
[Section titled “scroll\_debounce”](#scroll_debounce)
**type:** `float`
Debounce delay in seconds for auto-scrolling after streamed message content changes.
Default: `0.1`
Use a lower value for more immediate scrolling, or a higher value to reduce scroll updates during very fast token streams.
```python
mr.Chat(scroll_debounce=0.05)
mr.Chat(scroll_debounce=0)
```
***
## Chat Methods
[Section titled “Chat Methods”](#chat-methods)
### add()
[Section titled “add()”](#add)
Add a `Message` to the chat and scroll to it.
```python
chat.add(mr.Message(markdown="New message"))
```
***
### clear()
[Section titled “clear()”](#clear)
Remove all messages and show the placeholder.
```python
chat.clear()
```
***
## What Chat Does Not Do
[Section titled “What Chat Does Not Do”](#what-chat-does-not-do)
To avoid confusion, it is important to note that `Chat`:
* does not format messages
* does not limit message types
* does not manage user input
* does not impose a conversation structure
It is a **lightweight container**, not a full chat application framework.
***
## Notes
[Section titled “Notes”](#notes)
* `Chat` manages **ordering and scrolling only**
* All rendering logic lives in the `Message` widget
* Auto-scrolling is resilient to large outputs (plots, images)
* Internally, scrolling is implemented using a small frontend helper widget
# ChatInput
> How to use the ChatInput widget to collect user messages in a Mercury App
The **ChatInput** widget provides a multiline text input field with a send button, designed for chat-style Mercury Apps. It is typically used together with [`Chat`](./chat) and [`Message`](./message) to build conversational interfaces.
By default, `ChatInput` is displayed **at the bottom of the main view**.
***
## Basic Usage
[Section titled “Basic Usage”](#basic-usage)
Create a chat input and read the submitted message:
```python
import mercury as mr
# ChatInput is displayed at the bottom of the main view by default
prompt = mr.ChatInput()
if prompt.value:
# do something with user prompt
print("User said:", prompt.value)
```
## How Submission Works ⭐
[Section titled “How Submission Works ⭐”](#how-submission-works)
`prompt.value` contains the **last submitted message**.
A typical Mercury App flow looks like this:
1. User types a message and clicks the send button (or presses Enter).
2. `prompt.value` is set to the submitted text.
3. Your notebook code runs and reads `prompt.value`.
4. After the notebook finishes executing, Mercury resets `prompt.value` back to `""` so the next run starts clean.
Use `Shift+Enter` to insert a new line. If `send_on_enter=False`, Enter inserts new lines instead of submitting the message.
While the app is generating a response, the send button automatically changes to `Stop`. Clicking it interrupts the running kernel execution and preserves the text currently typed in the input.
Note
This reset behavior makes it easy to treat `prompt.value` as a “one-shot” input: it is available during the run, and cleared afterwards.
***
## Example: Chat + ChatInput
[Section titled “Example: Chat + ChatInput”](#example-chat--chatinput)
A minimal chat loop:
```python
import mercury as mr
chat = mr.Chat()
prompt = mr.ChatInput()
if prompt.value:
# user message
chat.add(mr.Message(markdown=prompt.value, role="user", emoji="👤"))
# assistant response
chat.add(mr.Message(markdown="Got it ✅", role="assistant", emoji="🤖"))
```
## Layout
[Section titled “Layout”](#layout)
Use the `position` argument to control where the widget is rendered:
* `"sidebar"` — in the sidebar
* `"inline"` — directly in the notebook output flow
* `"bottom"` — at the bottom of the main view (**default**)
```python
prompt = mr.ChatInput(position="inline")
```
***
## ChatInput Props
[Section titled “ChatInput Props”](#chatinput-props)
### value
[Section titled “value”](#value)
**type:** `string`
The last submitted message.
* Updated only when the user submits
* Reset to `""` after all cells are executed
***
### placeholder
[Section titled “placeholder”](#placeholder)
**type:** `string`
Placeholder text shown inside the input.
Default: `"Type a message..."`
***
### button\_icon
[Section titled “button\_icon”](#button_icon)
**type:** `string`
Text (or emoji) displayed on the send button.
Default: `"➤"`
***
### send\_on\_enter
[Section titled “send\_on\_enter”](#send_on_enter)
**type:** `bool`
If `True`, pressing Enter submits the message.
Default: `True`
Use `Shift+Enter` to add a new line without submitting.
***
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the widget is rendered.
Default: `"bottom"`
***
### custom\_css
[Section titled “custom\_css”](#custom_css)
**type:** `string`
Additional CSS appended to the widget’s default styles.
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier to distinguish widgets with identical arguments.
Note
If the same code cell is run again, Mercury reuses the previous widget instance unless a different `key` is specified.
The `key` value is needed if widgets are created in a loop.
***
## Notes
[Section titled “Notes”](#notes)
* `ChatInput` is designed for short, single-line messages.
* The widget clears the visible input field immediately after submission.
* `value` is best treated as “consume once during the current run”.
# Message
> How to use the Message widget to display chat messages and rich outputs in a Mercury App
The **Message** widget represents a single chat message with an avatar and rich content. It supports **Markdown**, **plain text**, **raw HTML**, and — importantly — **any object that can be displayed in Mercury**.
This makes `Message` not only a chat bubble, but also a **general-purpose output container** for charts, plots, images, and tables.
The widget is most commonly used inside the `Chat` container, but it can also be displayed on its own.
## Live Demo
[Section titled “Live Demo”](#live-demo)
You can try the `Message` widget directly in this interactive example:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/chat.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/chat?no-navbar)
## Basic Usage
[Section titled “Basic Usage”](#basic-usage)
A simple message rendered with Markdown:
```python
import mercury as mr
msg = mr.Message(markdown="**Hello world!**")
msg
```
## Using Message with Chat
[Section titled “Using Message with Chat”](#using-message-with-chat)
The most common use case is adding messages to a `Chat` container:
```python
chat = mr.Chat()
chat.add(
mr.Message(
markdown="Hello! How can I help you?",
role="assistant",
emoji="🤖",
emoji_background="#e5e7eb"
)
)
```
Each `Message` is displayed with:
* an avatar (emoji)
* configurable avatar background color
* rich content rendering
## Generic Output Container
[Section titled “Generic Output Container”](#generic-output-container)
In addition to text, **`Message` can display any object that Mercury (IPython) knows how to render**, including:
* matplotlib / seaborn plots
* Plotly charts
* pandas DataFrames
* images
* custom widgets
This is achieved using the standard IPython `display()` mechanism.
### Displaying Charts and Plots
[Section titled “Displaying Charts and Plots”](#displaying-charts-and-plots)
You can use a `Message` as an output context manager:
```python
import matplotlib.pyplot as plt
import mercury as mr
msg = mr.Message()
chat.add(msg)
with msg:
plt.plot([1, 2, 3], [1, 4, 2])
plt.title("Simple chart")
plt.show()
```
The plot is rendered **inside the message bubble**, just like text.
### Displaying a pandas DataFrame
[Section titled “Displaying a pandas DataFrame”](#displaying-a-pandas-dataframe)
```python
import pandas as pd
import mercury as mr
df = pd.DataFrame({
"name": ["Alice", "Bob", "Charlie"],
"score": [82, 91, 77]
})
msg = mr.Message(markdown="Here are the results:")
chat.add(msg)
with msg:
display(df)
```
This is especially useful for:
* interactive data exploration
* explaining results step-by-step
* building conversational data apps
***
Note
`Message` internally wraps an `ipywidgets.Output`, so anything that works with `display()` or `print()` can be rendered inside it.
## Rendering Modes
[Section titled “Rendering Modes”](#rendering-modes)
A `Message` can render content in **exactly one text mode at a time**:
* Markdown
* Plain text
* Raw HTML
These modes control **text rendering only**. Rich outputs (plots, tables, images) are always displayed using `display()`.
### Markdown
[Section titled “Markdown”](#markdown)
Messages support Markdown formatting, including code blocks and tables. Mercury automatically removes scripts and custom HTML styles from messages, including streamed responses added with `append_markdown()`.
Math typesetting and code syntax highlighting are not available in the default renderer. For content you write and trust, `mr.Message(..., unsafe_allow_html=True)` uses the original Jupyter renderer. This also allows JavaScript, so leave it off for user messages and AI responses.
```python
msg = mr.Message()
msg.set_content(markdown="### Title\nSome **bold** text")
```
### Plain Text
[Section titled “Plain Text”](#plain-text)
```python
msg = mr.Message()
msg.set_content(text="This is plain text.")
```
### Raw HTML
[Section titled “Raw HTML”](#raw-html)
```python
msg = mr.Message()
msg.set_content(html="Error")
```
Note
Raw HTML can run JavaScript. Use `html=` and `append_html()` only for content you write and trust. For user messages or AI responses, use Markdown instead.
## Streaming / Incremental Updates
[Section titled “Streaming / Incremental Updates”](#streaming--incremental-updates)
The `Message` widget supports incremental updates, which is useful for LLM streaming responses.
```python
msg = mr.Message()
chat.add(msg)
msg.append_markdown("Thinking")
msg.append_markdown("...")
msg.append_markdown("\n\nDone!")
```
Each call appends content and re-renders the message.
## Animated Text Helpers
[Section titled “Animated Text Helpers”](#animated-text-helpers)
### Bouncing Text
[Section titled “Bouncing Text”](#bouncing-text)
Render animated bouncing text (useful for loading indicators):
```python
msg = mr.Message()
msg.set_bouncing_text("Thinking...")
```
### Gradient Text
[Section titled “Gradient Text”](#gradient-text)
Render animated gradient text cycling through colors:
```python
msg = mr.Message()
msg.set_gradient_text(
"Processing",
colors=["#666", "#999", "#bbb"],
speed=1.0
)
```
## Message Props
[Section titled “Message Props”](#message-props)
### markdown
[Section titled “markdown”](#markdown-1)
**type:** `string`
Initial Markdown content rendered when the message is created.
***
### role
[Section titled “role”](#role)
**type:** `string`
Used to determine message role. Common values:
* `"user"`
* `"assistant"`
***
### emoji
[Section titled “emoji”](#emoji)
**type:** `string`
Emoji displayed in the avatar. The default is `"👤"`.
***
### emoji\_background
[Section titled “emoji\_background”](#emoji_background)
**type:** `string`
Avatar background color as a hex string. The default is a hardcoded gray: `#e5e7eb`.
Example:
```python
msg = mr.Message(
markdown="Hello!",
emoji="🤖",
emoji_background="#dbeafe"
)
```
***
## Message Methods
[Section titled “Message Methods”](#message-methods)
### set\_content()
[Section titled “set\_content()”](#set_content)
Replace message content entirely.
```python
msg.set_content(markdown="New content")
```
Exactly one argument must be provided:
* `markdown`
* `text`
* `html`
***
### append\_markdown()
[Section titled “append\_markdown()”](#append_markdown)
Append Markdown content and re-render.
```python
msg.append_markdown(" more text")
```
***
### append\_text()
[Section titled “append\_text()”](#append_text)
Append plain text (no formatting).
```python
msg.append_text(" raw text")
```
***
### append\_html()
[Section titled “append\_html()”](#append_html)
Append raw HTML.
```python
msg.append_html("
HTML")
```
***
### clear()
[Section titled “clear()”](#clear)
Remove all content from the message.
```python
msg.clear()
```
***
## Notes
[Section titled “Notes”](#notes)
* `Message` is both a **chat message** and a **generic output container**.
* Any object supported by `display()` can be rendered inside a message.
* Text rendering mode affects only text content.
* Auto-scrolling is handled by the parent `Chat` widget.
* Messages share a common CSS class: `mljar-chat-msg`.
# Scenarios
> Save and compare what-if analyses with values, tables, and plot snapshots
`mr.Scenarios` lets app users save named versions of an analysis, reload their inputs, and compare their saved results. Scalars, pandas/Polars DataFrames, and static Matplotlib plots can be compared together.
## Basic usage
[Section titled “Basic usage”](#basic-usage)
Put each input in its own cell. Put `Scenarios` in a cell **after all calculations** whose results you want to capture. Its controls appear in the sidebar by default, regardless of that cell’s position in the notebook.
```python
# %%
import mercury as mr
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display
# %%
growth = mr.Slider(value=5, min=0, max=30, label="Monthly growth (%)")
# %%
price = mr.NumberInput(value=50, min=10, max=100, label="Price ($)")
# %%
months = list(range(1, 13))
units = [1000 * (1 + growth.value / 100) ** (month - 1) for month in months]
forecast = pd.DataFrame({
"Month": months,
"Revenue": [round(count * price.value, 2) for count in units],
})
revenue = forecast["Revenue"].sum()
profit = revenue * 0.3
fig, ax = plt.subplots(figsize=(6, 3))
ax.plot(months, forecast["Revenue"], marker="o")
ax.set(xlabel="Month", ylabel="Revenue ($)", title="Monthly forecast")
plt.close(fig)
# %%
display(fig)
# %%
mr.Table(forecast)
# %%
scenarios = mr.Scenarios(
inputs={"Growth": growth, "Price": price},
outputs={
"Revenue": f"${revenue:,.0f}",
"Profit": f"${profit:,.0f}",
"Monthly forecast": forecast,
"Revenue chart": fig,
},
key="forecast-scenarios",
)
```
Install `matplotlib` to run the plot example. Polars is optional and is imported only when your notebook uses it. Pass the input **widgets**, rather than their `.value` attributes. Dictionary keys are display labels; widget `key` or `url_key` arguments are not required.
## Saving
[Section titled “Saving”](#saving)
Click **Save** beside the scenario selector, enter a name, and confirm **Save**. Reusing a name asks for confirmation before replacing the saved snapshot. A snapshot contains its name, save time, input values, and the outputs from the latest successful calculation.
Changing inputs, an execution in progress, or a failed execution prevents saving outdated results. Re-run the analysis through the `Scenarios` cell to make saving available again. Saving and comparing do not execute notebook cells.
Snapshots are independent copies. Changing a DataFrame, modifying a figure, or re-executing the notebook does not alter already saved scenarios. The component captures output data when its cell runs; **Save** stores that completed snapshot. Passing outputs to `Scenarios` does not display them in the main app. Use `display(fig)` and `mr.Table(forecast)` in separate cells to show the current plot and table as well as keeping them in saved comparisons.
## Loading
[Section titled “Loading”](#loading)
Choose a saved scenario and click **Load**. Mercury validates all its inputs before applying them together. Changed widget types, missing fields, unavailable choices, disabled inputs, or values outside current limits prevent loading and display an explanation. Validating fails before any inputs are changed.
In a Mercury app, one automatic execution starts **below the earliest changed input cell**, not below the `Scenarios` cell. With automatic execution disabled, click the app’s normal **Run** button after loading. In a regular Jupyter notebook, loading populates the widgets; run the calculation cells manually.
Loading recalculates current results. The scenario’s saved results remain intact, so comparison continues to show what was saved even if source data has changed. In shared-session apps, loading changes the shared inputs for everyone using that kernel, while the saved scenario collection remains local to each browser.
## Comparing values, tables, and plots
[Section titled “Comparing values, tables, and plots”](#comparing-values-tables-and-plots)
Click **Compare** and select two to four saved scenarios.
* Scalars appear in one table with inputs first and outputs second. Values that differ from the first selected scenario are highlighted.
* Each DataFrame output appears in its own section, with scenario tables next to each other. Each table has independent column sorting and pagination (10 rows per page). Named or non-default pandas indexes are shown as row labels.
* Matplotlib Figures and Axes appear as static PNG snapshots next to each other. Plot axes and scales are captured as drawn; use consistent limits in your notebook when visual comparisons require the same scale.
On mobile, table and plot panels stack vertically, and wide tables scroll within their panels. Tables are compared visually; rows are not automatically joined or matched across scenarios. Interactive Plotly/Altair objects are not accepted in this version; pass a Matplotlib figure for a plot snapshot.
Scalar values can be strings, numbers, booleans, `None`, dates, datetimes, or common NumPy scalars. Table cells use these same types. Missing values and non-finite numbers display as `—`. Integers outside JavaScript’s exact range retain their digits. Format scalar outputs as strings when you want currency or percentage formatting. Nested table cells and arbitrary Python objects produce a validation message rather than being converted to executable HTML or silently stringified.
## Browser storage
[Section titled “Browser storage”](#browser-storage)
Scenarios are stored in IndexedDB in the current browser profile, scoped to the app and component key. They survive refreshing the app and restarting its kernel. Use a stable `key` when editing or moving the component’s notebook cell; without one, Mercury uses the source cell ID where available. Give separate components different keys. Changing the app URL or key selects a different collection.
They are not written to the notebook or synchronized across devices. Clearing the site’s browser data removes them, and private browsing may discard them when the window closes. **Delete** removes an individual scenario after confirmation.
Limits are 5,000 rows and 100 columns per table, 5 MiB per snapshot, and 20 scenarios or 20 MiB per component. Oversized snapshots are rejected without truncating data. If browser storage is unavailable or full, the component displays an error.
## Arguments
[Section titled “Arguments”](#arguments)
| Argument | Description | Default |
| ---------- | --------------------------------------------------------------------------- | ----------------------------- |
| `inputs` | Mapping of display labels to Mercury input widgets | Required |
| `outputs` | Mapping of display labels to values, DataFrames, or Matplotlib Figures/Axes | Required |
| `position` | `"sidebar"`, `"inline"`, or `"bottom"` | `"sidebar"` |
| `key` | Stable component and storage identifier | Source cell ID when available |
Controls and comparison dialogs use Mercury’s fonts, colors, borders, and corner radius from `config.toml`.
# Stop
> How to stop execution in a Mercury App with mr.Stop
The **Stop** helper lets you stop the current notebook execution flow **silently** inside a Mercury App. It is useful for early exits like validation, missing inputs, or conditional flows (for example: stop until the user uploads a file).
## Usage
[Section titled “Usage”](#usage)
Call `mr.Stop()` to stop execution immediately.
### Basic Example
[Section titled “Basic Example”](#basic-example)
**Code**
```python
import mercury as mr
x = 0
if x == 0:
mr.Stop()
print("This line will not run")
```
### Typical Pattern: Stop until a widget has a value
[Section titled “Typical Pattern: Stop until a widget has a value”](#typical-pattern-stop-until-a-widget-has-a-value)
This is a common pattern in Mercury Apps: render a widget, and stop execution until the user provides input.
**Code**
```python
import mercury as mr
uploader = mr.UploadFile(label="Upload CSV")
if uploader.value is None:
mr.Stop()
print("File uploaded:", uploader.name)
```
### Validation Guard
[Section titled “Validation Guard”](#validation-guard)
Use `mr.Stop()` to prevent running expensive code when inputs are invalid.
**Code**
```python
import mercury as mr
threshold = mr.NumberInput(label="Threshold", value=0.5, min=0.0, max=1.0, step=0.05)
if threshold.value < 0.2:
# too small — stop here
mr.Stop()
print("Running with threshold:", threshold.value)
```
## API
[Section titled “API”](#api)
### mr.Stop()
[Section titled “mr.Stop()”](#mrstop)
Stops the current notebook execution flow. The rest of the current cell and all following cells are skipped.
* It raises an internal exception used only for flow control.
* It does **not** show a traceback.
* It does **not** display an error message.
***
## Notes
[Section titled “Notes”](#notes)
* `mr.Stop()` is intended for **control flow**, not for error handling.
* If you need to show a message to the user, display it **before** calling `mr.Stop()`.
Note
`mr.Stop()` stops only the current execution flow. When the user changes a widget, Mercury may re-run the notebook, and execution can continue once your condition is satisfied.
# Customization
> Customize the look and feel of Mercury web apps
Mercury apps can be customized with a `config.toml` file placed in the active notebooks directory.
* If you start Mercury in a directory, put `config.toml` there.
* If you start Mercury with `--working-dir`, Mercury loads `config.toml` from that working directory.
## Basic app settings
[Section titled “Basic app settings”](#basic-app-settings)
```toml
[main]
title = "Mercury"
footer = "MLJAR - next generation of AI tools"
favicon_emoji = "🎉"
notebooks_button_label = "Notebooks"
starting_message = "Initializing web application…"
starting_icon = "spinner"
search_filter_label = "Search notebooks"
show_search_filter = true
thumbnail_text = "📘"
thumbnail_bg = "#f1f5f9"
thumbnail_text_color = "#0f172a"
[welcome]
header = "Welcome"
message = "Choose a notebook below."
```
Available settings:
* `main.title` - title shown in the app navbar and listing page
* `main.footer` - footer text
* `main.favicon_emoji` - emoji used as the browser tab icon
* `main.notebooks_button_label` - label shown on the notebooks dropdown button in the navbar
* `main.starting_message` - text shown on the startup loading overlay before the app is ready
* `main.starting_icon` - startup icon shown in the loading overlay: `coffee`, `spinner`, or `none`
* `main.search_filter_label` - label and placeholder shown in the notebook search field
* `main.show_search_filter` - whether the notebook search field should be displayed
* `main.thumbnail_text` - emoji or text displayed on notebook thumbnails
* `main.thumbnail_bg` - background color for notebook thumbnails
* `main.thumbnail_text_color` - text color for notebook thumbnails
* `welcome.header` - optional welcome header on the notebook listing page
* `welcome.message` - optional welcome message on the notebook listing page
Notebook thumbnails can still be overridden per notebook through notebook metadata. Values from `[main]` act as defaults for the whole notebooks directory.
## Theming
[Section titled “Theming”](#theming)
Mercury uses a compact theme API. Most apps only need a few tokens in `[theme]`, and Mercury derives the rest.
```toml
[theme]
font = "Inter, ui-sans-serif, system-ui, sans-serif"
font_size = "15px"
background_color = "#f8fafc"
surface_color = "#ffffff"
text_color = "#0f172a"
primary_color = "#2563eb"
```
These core tokens affect the whole app:
* notebook listing page
* notebook app navbar
* sidebar
* widgets
* tables
* Markdown output
* loader and toasts
### Core theme tokens
[Section titled “Core theme tokens”](#core-theme-tokens)
* `font` - base font stack for pages and widgets
* `font_size` - base UI font size
* `background_color` - outer page background
* `surface_color` - cards, panels, widgets, and content surfaces
* `text_color` - main text color
* `primary_color` - primary accent and interactive color
Mercury derives many other values from these automatically, including:
* muted text
* borders
* focus color
* hover and selected states
* sidebar colors
* top navbar colors
* footer colors
* widget surfaces
* button and run button styling
## Web fonts
[Section titled “Web fonts”](#web-fonts)
If you want to use a Google Font or another hosted stylesheet, add `font_url`.
```toml
[theme]
font = "Inter, ui-sans-serif, system-ui, sans-serif"
font_url = "https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap"
background_color = "#f8fafc"
surface_color = "#ffffff"
text_color = "#0f172a"
primary_color = "#2563eb"
```
Notes:
* `font` only sets the CSS `font-family`
* `font_url` tells Mercury to load the stylesheet in the app pages
* for Google Fonts, Mercury also adds the standard preconnect tags
## Advanced overrides
[Section titled “Advanced overrides”](#advanced-overrides)
If you need more control, use `[theme.overrides]`.
```toml
[theme]
font = "IBM Plex Sans, ui-sans-serif, system-ui, sans-serif"
font_size = "14px"
background_color = "#0b1220"
surface_color = "#111827"
text_color = "#e5e7eb"
primary_color = "#22c55e"
[theme.overrides]
topbar_background_color = "#111827"
topbar_text_color = "#f8fafc"
sidebar_background_color = "#0f172a"
sidebar_text_color = "#e5e7eb"
focus_border_color = "#38bdf8"
border_radius = "12px"
```
Useful override keys:
* `accent_color`
* `focus_border_color`
* `border_color`
* `muted_text_color`
* `hover_background_color`
* `selected_background_color`
* `topbar_background_color`
* `topbar_text_color`
* `topbar_border_color`
* `sidebar_background_color`
* `sidebar_text_color`
* `sidebar_title_color`
* `footer_background_color`
* `footer_text_color`
* `widget_background_color`
* `card_background_color`
* `run_button_background`
* `run_button_background_hover`
* `run_button_text_color`
* `border_radius`
## Examples
[Section titled “Examples”](#examples)
### Minimal
[Section titled “Minimal”](#minimal)
```toml
[main]
title = "Mercury"
footer = "Internal tools"
favicon_emoji = "⚡"
[welcome]
header = "Welcome"
message = "Choose an app below."
[theme]
font = "Inter, ui-sans-serif, system-ui, sans-serif"
font_size = "15px"
background_color = "#f8fafc"
surface_color = "#ffffff"
text_color = "#0f172a"
primary_color = "#2563eb"
```
### Darkish
[Section titled “Darkish”](#darkish)
```toml
[main]
title = "Ops Console"
footer = "Platform Engineering"
favicon_emoji = "🛠️"
[welcome]
header = "Operations"
message = "Run notebooks as internal apps."
[theme]
font = "IBM Plex Sans, ui-sans-serif, system-ui, sans-serif"
font_size = "14px"
background_color = "#0b1220"
surface_color = "#111827"
text_color = "#e5e7eb"
primary_color = "#22c55e"
[theme.overrides]
accent_color = "#38bdf8"
topbar_background_color = "#111827"
topbar_text_color = "#f8fafc"
sidebar_background_color = "#0f172a"
sidebar_text_color = "#e5e7eb"
sidebar_title_color = "#f8fafc"
```
### Editorial
[Section titled “Editorial”](#editorial)
```toml
[main]
title = "Research Hub"
footer = "Notebook publishing"
favicon_emoji = "📘"
[welcome]
header = "Research Apps"
message = "Interactive notebooks with a calmer visual style."
[theme]
font = "Source Serif 4, Georgia, serif"
font_url = "https://fonts.googleapis.com/css2?family=Source+Serif+4:wght@400;600;700;800&display=swap"
font_size = "16px"
background_color = "#fcfaf7"
surface_color = "#fffdf9"
text_color = "#2f241f"
primary_color = "#b45309"
[theme.overrides]
topbar_background_color = "#3f2d23"
topbar_text_color = "#fff7ed"
sidebar_background_color = "#f6efe7"
sidebar_text_color = "#2f241f"
sidebar_title_color = "#2f241f"
```
## What gets themed
[Section titled “What gets themed”](#what-gets-themed)
The theme is applied consistently across:
* notebook listing page
* notebook app navbar
* app sidebar
* text, number, select, multiselect, date, time, and datetime widgets
* tables
* Markdown output
* buttons, tabs, expanders, file upload, slider, checkbox, download
* loader and toast messages
## Compatibility
[Section titled “Compatibility”](#compatibility)
Mercury still accepts several older or alternative keys and normalizes them internally.
Examples:
* `backgroundColor`
* `secondaryBackgroundColor`
* `textColor`
* `primaryColor`
* `accentColor`
* `surfaceColor`
* `fontFamily`
* `fontUrl`
Legacy Mercury keys such as `panel_bg`, `widget_background_color`, or `sidebar_background_color` also continue to work.
## Example files
[Section titled “Example files”](#example-files)
Example configs are available in the repository:
* `config.minimal.toml`
* `config.darkish.toml`
* `config.editorial.toml`
* `config.google-font.toml`
If something important is still not customizable enough, please open an issue.
# Button
> How to use the Button widget in the interactive Mercury App
The **Button** widget displays a clickable button that allows users to **trigger actions** in a Mercury App. It is useful for running computations, refreshing results, submitting parameters, or controlling app logic.
## Live Demo
[Section titled “Live Demo”](#live-demo)
You can try the `Button` widget directly in this interactive example:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/input-widgets.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/input-widgets?no-navbar)
## Usage
[Section titled “Usage”](#usage)
To create a Button widget, you only need to provide a label. When the user clicks the button, its state is updated and can be inspected from Python.
### Basic Example
[Section titled “Basic Example”](#basic-example)
**Code**
```python
import mercury as mr
btn = mr.Button(
label="Run"
)
```
To check whether the button has been clicked:
```python
btn.value
```
After a click, the value becomes:
```text
True
```
You can also inspect how many times the button was clicked:
```python
btn.n_clicks
```
***
### Variants and Sizes
[Section titled “Variants and Sizes”](#variants-and-sizes)
You can customize the button appearance using `variant` and `size` arguments.
**Code**
```python
mr.Button(
label="Delete",
variant="danger",
size="lg"
)
```
Available variants:
* `"primary"` (default)
* `"secondary"`
* `"outline"`
* `"danger"`
Available sizes:
* `"sm"`
* `"md"` (default)
* `"lg"`
***
### Layout
[Section titled “Layout”](#layout)
Use the `position` argument to control where the button is displayed. The default is `position="sidebar"`.
Available positions:
* `"sidebar"` — displayed in the left sidebar (default)
* `"inline"` — displayed in the main notebook output
* `"bottom"` — displayed after all notebook cells
**Code**
```python
mr.Button(
label="Run analysis",
position="inline"
)
```
***
## Button Props
[Section titled “Button Props”](#button-props)
### label
[Section titled “label”](#label)
**type:** `string`
Text displayed on the button. The default is `"Run"`.
***
### value
[Section titled “value”](#value)
**type:** `bool`
Indicates whether the button has been clicked.
* Initially `False`
* Set to `True` after the first click
The value stays `True` until you reset it manually in Python.
***
### n\_clicks
[Section titled “n\_clicks”](#n_clicks)
**type:** `int`
Counts how many times the button has been clicked. Starts at `0`.
***
### last\_clicked\_at
[Section titled “last\_clicked\_at”](#last_clicked_at)
**type:** `string`
ISO timestamp of the last click. Empty string before the first click.
***
### variant
[Section titled “variant”](#variant)
**type:** `"primary" | "secondary" | "outline" | "danger"`
Controls the visual style of the button. The default is `"primary"`.
***
### size
[Section titled “size”](#size)
**type:** `"sm" | "md" | "lg"`
Controls the button size. The default is `"md"`.
***
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the widget is rendered:
* **sidebar** — in the sidebar (default)
* **inline** — directly in the notebook cell output
* **bottom** — after all notebook cells
***
### disabled
[Section titled “disabled”](#disabled)
**type:** `bool`
If `True`, the button is visible but cannot be clicked. The default is `False`.
***
### hidden
[Section titled “hidden”](#hidden)
**type:** `bool`
If `True`, the widget exists in the UI state but is not rendered. The default is `False`.
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to distinguish widgets with identical arguments.
Note
If the same code cell is run again, Mercury reuses the previous widget instance unless a different `key` is specified.
The `key` value is required when widgets are created inside loops.
***
## Notes
[Section titled “Notes”](#notes)
* The button does not return a value automatically — instead, it updates its internal state.
* Use `.value`, `.n_clicks`, or `.last_clicked_at` to react to user clicks.
* A common pattern is to check `if btn.value:`. The `value` is set back to `False` after cells re-execution.
# Camera
> Show a live browser camera preview and capture photos or short videos in a Mercury App
The **Camera** input widget displays a live camera preview directly in the notebook and Mercury App. It can capture a photo or record a short video and makes the completed result available to Python as bytes.
## Live photo camera
[Section titled “Live photo camera”](#live-photo-camera)
Put the camera and the code that reads its value in separate notebook cells:
```python
# %%
import mercury as mr
# %%
camera = mr.Camera(
mode="photo",
label="Document camera",
)
# %%
if camera.value is not None:
print(camera.filename)
print(camera.mime_type)
print(camera.size)
```
The browser requests camera permission when the widget appears. Once permission is granted, the live feed is shown inside the widget. Select **Take photo** to freeze the frame or **Retake** to return to the live preview and clear the current result.
The live preview stays entirely in the browser. Preview frames are not sent to the Mercury server and do not execute notebook cells.
## Read a photo in Python
[Section titled “Read a photo in Python”](#read-a-photo-in-python)
`camera.value` contains the captured bytes. For example, open a photo with Pillow:
```python
# %%
from io import BytesIO
from PIL import Image
if camera.value is not None:
image = Image.open(BytesIO(camera.value))
display(image)
```
## Record a short video
[Section titled “Record a short video”](#record-a-short-video)
Use `mode="video"` to show the same live preview with recording controls:
```python
# %%
camera = mr.Camera(
mode="video",
label="Record a sample",
max_duration=15,
audio=False,
)
```
Select **Start recording** and **Stop recording**. Recording stops automatically at `max_duration`. The completed video can be reviewed in the widget before it is retaken.
The browser selects a supported recording format. Chromium and Firefox commonly use WebM, while Safari may use MP4, so inspect `camera.mime_type` instead of assuming a specific format.
## Front and rear cameras
[Section titled “Front and rear cameras”](#front-and-rear-cameras)
Prefer the rear camera for documents, OCR, and object recognition:
```python
camera = mr.Camera(facing_mode="environment")
```
Prefer the front camera for webcam applications:
```python
camera = mr.Camera(facing_mode="user")
```
`facing_mode` is a browser preference. The available camera hardware determines which device is ultimately selected.
## Reactive execution
[Section titled “Reactive execution”](#reactive-execution)
The camera updates Python only after a complete action:
* **Take photo** sends one image buffer.
* **Stop recording** sends one completed video buffer.
* Automatic video completion sends one completed video buffer.
* **Retake** clears the current result.
Each action changes the widget once and executes cells below the Camera cell once. A live preview or an in-progress recording does not execute notebook code.
## Result properties
[Section titled “Result properties”](#result-properties)
### `value`
[Section titled “value”](#value)
Captured image or video data as `bytes`, or `None` before capture and after Retake.
### `mime_type`
[Section titled “mime\_type”](#mime_type)
The browser-reported MIME type, such as `"image/jpeg"`, `"video/webm"`, or `"video/mp4"`. It is an empty string when no capture is available.
### `filename`
[Section titled “filename”](#filename)
A generated timestamped filename with an extension matching the captured format.
### `size`
[Section titled “size”](#size)
Size of the captured data in bytes.
### `duration`
[Section titled “duration”](#duration)
Recorded video duration in seconds. It is `0` for photos.
## Parameters
[Section titled “Parameters”](#parameters)
### `mode`
[Section titled “mode”](#mode)
`"photo"` or `"video"`. Default: `"photo"`.
### `label`
[Section titled “label”](#label)
Text displayed above the preview. Default: `"Camera"`. Use an empty string to hide the label.
### `facing_mode`
[Section titled “facing\_mode”](#facing_mode)
Preferred camera direction: `"environment"` or `"user"`. Default: `"environment"`.
### `max_duration`
[Section titled “max\_duration”](#max_duration)
Maximum video recording length from 1 to 300 seconds. Default: `30`.
### `audio`
[Section titled “audio”](#audio)
Request microphone audio with a video recording. Default: `False`. This option is ignored in photo mode.
### `position`
[Section titled “position”](#position)
Mercury layout placement: `"inline"`, `"sidebar"`, or `"bottom"`. Default: `"inline"`.
### `disabled`
[Section titled “disabled”](#disabled)
Disable controls and stop the active media stream. Default: `False`.
### `hidden`
[Section titled “hidden”](#hidden)
Hide the widget and stop the active media stream. Default: `False`.
### `key`
[Section titled “key”](#key)
Stable identifier used to reuse the widget across reactive cell executions.
## Permissions and deployment
[Section titled “Permissions and deployment”](#permissions-and-deployment)
Browsers allow camera access only from a secure context: an HTTPS deployment or `localhost` during development. The user must grant permission for the app’s browser origin. Mercury cannot override denied browser permission.
Camera and microphone tracks are stopped when the widget is hidden, disabled, or removed from the page. Captured media is transferred directly through the notebook widget connection; Mercury does not automatically write it to a temporary file.
## Theme configuration
[Section titled “Theme configuration”](#theme-configuration)
The label, controls, borders, colors, shadows, and corner radius use the Mercury theme loaded from `config.toml`, including `primary_color`, `danger_color`, `border_color`, `border_radius`, and `border_radius_lg`.
# CheckBox
> How to use the CheckBox widget in the interactive Mercury App
The **CheckBox** widget displays a boolean control that lets users toggle a value between **True** and **False**. It is useful for enabling/disabling features, turning options on/off, or controlling conditional logic in your Mercury App.
## Live Demo
[Section titled “Live Demo”](#live-demo)
You can try the `CheckBox` widget directly in this interactive example:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/input-widgets.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/input-widgets?no-navbar)
## Usage
[Section titled “Usage”](#usage)
To create a CheckBox widget, provide a `label`. The current state is always available via `.value`.
### Basic Example
[Section titled “Basic Example”](#basic-example)
**Code**
```python
import mercury as mr
cb = mr.CheckBox(
label="Auto-refresh"
)
```
To get the current value:
```python
cb.value
```
***
### Appearance
[Section titled “Appearance”](#appearance)
The CheckBox supports two visual styles controlled by the `appearance` argument:
* `"toggle"` — switch-style control (default)
* `"box"` — classic square checkbox
**Code**
```python
import mercury as mr
cb_toggle = mr.CheckBox(
label="Auto-refresh",
appearance="toggle"
)
cb_box = mr.CheckBox(
label="I agree",
appearance="box"
)
```
To read the state:
```python
cb_toggle.value
cb_box.value
```
***
### Layout
[Section titled “Layout”](#layout)
Use the `position` argument to control where the widget is displayed. The default is `position="sidebar"`.
Available positions:
* `"sidebar"` — displayed in the left sidebar (default)
* `"inline"` — displayed in the main notebook output
* `"bottom"` — displayed after all notebook cells
**Code**
```python
cb = mr.CheckBox(
label="Enable option",
position="inline"
)
```
***
## CheckBox Props
[Section titled “CheckBox Props”](#checkbox-props)
### label
[Section titled “label”](#label)
**type:** `string`
Text displayed next to the control. The default is `"Enable"`.
***
### value
[Section titled “value”](#value)
**type:** `bool`
Current checkbox state.
* `False` means unchecked/off
* `True` means checked/on
The value always reflects the latest user selection.
***
### appearance
[Section titled “appearance”](#appearance-1)
**type:** `"toggle" | "box"`
Controls how the checkbox is rendered:
* **toggle** — switch-style control (default)
* **box** — classic square checkbox
***
### url\_key
[Section titled “url\_key”](#url_key)
**type:** `string`
Name of the URL query parameter used to override the initial value.
* If the URL contains a valid boolean value for this key, it takes precedence over `value`.
* Missing, empty, or invalid URL values fall back to `value`.
* Accepted values are `true` and `false`, matched case-insensitively.
Example:
```text
?enabled=true
```
***
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the widget is rendered:
* **sidebar** — in the sidebar (default)
* **inline** — directly in the notebook cell output
* **bottom** — after all notebook cells
***
### disabled
[Section titled “disabled”](#disabled)
**type:** `bool`
If `True`, the widget is visible but cannot be interacted with. The default is `False`.
***
### hidden
[Section titled “hidden”](#hidden)
**type:** `bool`
If `True`, the widget exists in the UI state but is not rendered. The default is `False`.
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to distinguish widgets with identical arguments.
Note
If the same code cell is run again, Mercury reuses the previous widget instance unless a different `key` is specified.
The `key` value is required when widgets are created inside loops.
***
## Notes
[Section titled “Notes”](#notes)
* The widget value is always a **boolean**.
* `url_key` can be used to initialize the widget from URL query parameters.
* URL parameters accept only `true` and `false` values, matched case-insensitively.
* Use `.value` in Python to access the current state.
# DateInput
> How to use the DateInput widget in the interactive Mercury App
The **DateInput** widget displays a native browser date picker. Its value is available as an ISO date string: `YYYY-MM-DD`.
## Usage
[Section titled “Usage”](#usage)
```python
import mercury as mr
date = mr.DateInput(
label="Select date",
value="2026-04-30"
)
date.value
```
## DateInput Props
[Section titled “DateInput Props”](#dateinput-props)
### label
[Section titled “label”](#label)
**type:** `string`
Text displayed above the input. The default is `"Date"`.
### value
[Section titled “value”](#value)
**type:** `string`
Initial date value in `YYYY-MM-DD` format.
### min
[Section titled “min”](#min)
**type:** `string`
Minimum allowed date in `YYYY-MM-DD` format.
### max
[Section titled “max”](#max)
**type:** `string`
Maximum allowed date in `YYYY-MM-DD` format.
### url\_key
[Section titled “url\_key”](#url_key)
**type:** `string`
URL query parameter name used to override the initial value.
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the widget is rendered. The default is `"sidebar"`.
### disabled
[Section titled “disabled”](#disabled)
**type:** `bool`
If `True`, the input is visible but cannot be edited.
### hidden
[Section titled “hidden”](#hidden)
**type:** `bool`
If `True`, the widget exists in app state but is not rendered.
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to distinguish widgets with identical arguments.
# DateRange
> How to use the DateRange widget in the interactive Mercury App
The **DateRange** widget displays two native browser date inputs: start and end. Its value is available as a list of ISO date strings: `["YYYY-MM-DD", "YYYY-MM-DD"]`.
## Usage
[Section titled “Usage”](#usage)
```python
import mercury as mr
date_range = mr.DateRange(
label="Select date range",
value=("2026-04-01", "2026-04-30")
)
date_range.value
```
## DateRange Props
[Section titled “DateRange Props”](#daterange-props)
### label
[Section titled “label”](#label)
**type:** `string`
Text displayed above the inputs. The default is `"Date range"`.
### value
[Section titled “value”](#value)
**type:** `tuple[str, str] | list[str] | None`
Initial start and end dates in `YYYY-MM-DD` format.
### min
[Section titled “min”](#min)
**type:** `string`
Minimum allowed date for both inputs.
### max
[Section titled “max”](#max)
**type:** `string`
Maximum allowed date for both inputs.
### start\_url\_key
[Section titled “start\_url\_key”](#start_url_key)
**type:** `string`
URL query parameter name used to override the start date.
### end\_url\_key
[Section titled “end\_url\_key”](#end_url_key)
**type:** `string`
URL query parameter name used to override the end date.
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the widget is rendered. The default is `"sidebar"`.
### disabled
[Section titled “disabled”](#disabled)
**type:** `bool`
If `True`, both inputs are visible but cannot be edited.
### hidden
[Section titled “hidden”](#hidden)
**type:** `bool`
If `True`, the widget exists in app state but is not rendered.
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to distinguish widgets with identical arguments.
# DateTimeInput
> How to use the DateTimeInput widget in the interactive Mercury App
The **DateTimeInput** widget displays a native browser date-and-time picker. Its value is available as `YYYY-MM-DD HH:MM` or `YYYY-MM-DD HH:MM:SS`.
## Usage
[Section titled “Usage”](#usage)
```python
import mercury as mr
dt = mr.DateTimeInput(
label="Select date and time",
value="2026-04-30 14:30"
)
dt.value
```
## DateTimeInput Props
[Section titled “DateTimeInput Props”](#datetimeinput-props)
### label
[Section titled “label”](#label)
**type:** `string`
Text displayed above the input. The default is `"Date and time"`.
### value
[Section titled “value”](#value)
**type:** `string`
Initial datetime value in `YYYY-MM-DD HH:MM` or `YYYY-MM-DD HH:MM:SS` format.
### min
[Section titled “min”](#min)
**type:** `string`
Minimum allowed datetime.
### max
[Section titled “max”](#max)
**type:** `string`
Maximum allowed datetime.
### step
[Section titled “step”](#step)
**type:** `int`
Input step in seconds. The default is `60`.
### url\_key
[Section titled “url\_key”](#url_key)
**type:** `string`
URL query parameter name used to override the initial value.
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the widget is rendered. The default is `"sidebar"`.
### disabled
[Section titled “disabled”](#disabled)
**type:** `bool`
If `True`, the input is visible but cannot be edited.
### hidden
[Section titled “hidden”](#hidden)
**type:** `bool`
If `True`, the widget exists in app state but is not rendered.
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to distinguish widgets with identical arguments.
# Examples
> Populate Mercury input widgets from named predefined examples
The **Examples** component displays named presets that populate existing Mercury input widgets. It lets users try meaningful inputs without entering every value manually.
## Basic usage
[Section titled “Basic usage”](#basic-usage)
Define the input widgets first, place `Examples` after them, and put calculations below it. Each `# %%` starts a separate notebook cell:
```python
# %%
import mercury as mr
# %%
age = mr.NumberInput(
label="Age",
value=30,
min=18,
max=100,
key="age",
)
# %%
country = mr.Select(
label="Country",
value="Poland",
choices=["Poland", "Germany", "France"],
url_key="country",
)
# %%
presets = mr.Examples(
examples={
"Young customer": {
"age": 25,
"country": "Poland",
},
"Enterprise customer": {
"age": 45,
"country": "Germany",
},
}
)
# %%
print(age.value, country.value)
```
`Examples` appears in the sidebar by default. Clicking a named example updates all valid target inputs and then triggers one reactive execution of the cells below the component.
## Matching input widgets
[Section titled “Matching input widgets”](#matching-input-widgets)
Each field name is matched against an input widget’s exact, case-sensitive `key` first. If no key matches, Mercury looks for the same `url_key`:
```python
age = mr.NumberInput(key="age")
country = mr.Select(choices=["Poland", "Germany"], url_key="country")
```
The example identifiers are therefore `"age"` and `"country"`. Python variable names and visible labels are not used for matching.
If one widget uses `key="country"` while another uses `url_key="country"`, the key match wins. Reusing the same key or URL key across multiple inputs is ambiguous; that field is skipped and a warning is displayed.
## Execution behavior
[Section titled “Execution behavior”](#execution-behavior)
Mercury applies an example in three phases:
1. Resolve and validate every provided field.
2. Update every valid target widget.
3. Request one notebook execution after the updates are complete.
Missing, ambiguous, or invalid fields do not prevent other valid fields from being applied. If no value changes, the notebook is not rerun.
The component follows the app’s global auto-rerun setting. When auto-rerun is disabled, values are populated but execution waits for the normal **Run** action.
Cell order is important. Inputs must exist before `Examples` so they can be found, and code that consumes their values must be in cells below `Examples` so it is included in the single reactive execution.
## Warnings
[Section titled “Warnings”](#warnings)
Problems are shown below the example buttons and also emitted as Python warnings:
```text
⚠ Widget not found: customer_segment.
```
Warnings cover:
* missing widget identifiers;
* identifiers shared by multiple input widgets;
* values with the wrong type;
* numbers outside an input’s range;
* unknown Select or MultiSelect choices;
* invalid dates, times, or date ranges.
Warnings clear when another example is selected.
## Supported inputs
[Section titled “Supported inputs”](#supported-inputs)
The first version supports explicit values for:
* `TextInput`
* `NumberInput`
* `Slider`
* `Select`
* `MultiSelect`
* `CheckBox`
* `DateInput`
* `TimeInput`
* `DateTimeInput`
* `DateRange`
Target a `DateRange` by its widget key and provide its complete range:
```python
period = mr.DateRange(label="Period", key="reporting-period")
presets = mr.Examples({
"First quarter": {
"reporting-period": ["2026-01-01", "2026-03-31"],
}
})
```
Buttons, file uploads, layout widgets, output widgets, and arbitrary third-party ipywidgets are not targets in this version.
## Position
[Section titled “Position”](#position)
The default is `position="sidebar"`. Use `"inline"` or `"bottom"` when the examples should appear with notebook output:
```python
presets = mr.Examples(examples, position="inline")
```
Buttons are displayed vertically in dictionary insertion order. Their typography, colors, borders, selected state, focus ring, and warnings use runtime Mercury theme variables from `config.toml`, including `border_radius` for the button corners.
## Parameters
[Section titled “Parameters”](#parameters)
### `examples`
[Section titled “examples”](#examples)
Required non-empty mapping of example names to mappings of widget identifiers and explicit values. Example names and widget identifiers must be non-empty strings.
### `position`
[Section titled “position”](#position-1)
Mercury layout placement: `"sidebar"`, `"inline"`, or `"bottom"`. Default: `"sidebar"`.
### `key`
[Section titled “key”](#key)
Stable identifier used to reuse the component across cell executions.
# Microphone
> Record audio in the browser and access it as bytes in a Mercury App
The **Microphone** input widget records audio in the browser and makes the completed recording available to Python as bytes. It is useful for speech-to-text, voice assistants, transcription, audio classification, and multimodal applications.
## Basic usage
[Section titled “Basic usage”](#basic-usage)
Put the recorder and code that reads its value in separate notebook cells:
```python
# %%
import mercury as mr
# %%
microphone = mr.Microphone(
label="Record audio",
max_duration=60,
)
# %%
if microphone.value is not None:
print(microphone.filename)
print(microphone.mime_type)
print(microphone.size)
print(microphone.duration)
```
Select **Start recording** to request microphone permission and begin recording. Select **Stop recording** to finish. Recording also stops automatically when it reaches `max_duration`.
After recording, an audio player appears so the result can be reviewed. Select **Record again** to clear it and return to the initial state.
## Read audio in Python
[Section titled “Read audio in Python”](#read-audio-in-python)
`microphone.value` contains the complete recorded bytes:
```python
# %%
from io import BytesIO
if microphone.value is not None:
audio_data = BytesIO(microphone.value)
transcription = speech_to_text(audio_data)
print(transcription)
```
Libraries that require a path can be given an explicitly created temporary file:
```python
# %%
import tempfile
if microphone.value is not None:
suffix = ".mp4" if "mp4" in microphone.mime_type else ".webm"
with tempfile.NamedTemporaryFile(suffix=suffix) as recording:
recording.write(microphone.value)
recording.flush()
result = transcribe_file(recording.name)
```
Mercury does not automatically create a temporary file, so its ownership and lifetime remain explicit in notebook code.
## Recording format
[Section titled “Recording format”](#recording-format)
The browser selects a supported format. Chromium commonly produces Opus audio in a WebM container, Firefox may use WebM or Ogg, and Safari may use MP4. Always inspect `microphone.mime_type` instead of assuming a format.
## Reactive execution
[Section titled “Reactive execution”](#reactive-execution)
The microphone updates Python only after a complete action:
* **Stop recording** sends one complete audio buffer.
* Reaching `max_duration` sends one complete audio buffer.
* **Record again** clears the current result.
Each action changes the widget once and executes cells below the Microphone cell once. Waiting for permission and recording in progress do not execute notebook code or send live audio chunks to Python.
## Result properties
[Section titled “Result properties”](#result-properties)
### `value`
[Section titled “value”](#value)
Recorded audio as `bytes`, or `None` before recording and after Record again.
### `mime_type`
[Section titled “mime\_type”](#mime_type)
The browser-reported MIME type, such as `"audio/webm;codecs=opus"`, `"audio/ogg"`, or `"audio/mp4"`. It is empty when no recording is available.
### `filename`
[Section titled “filename”](#filename)
A generated timestamped filename with an extension matching the recording container.
### `size`
[Section titled “size”](#size)
Size of the recorded data in bytes.
### `duration`
[Section titled “duration”](#duration)
Recording duration in seconds.
## Parameters
[Section titled “Parameters”](#parameters)
### `label`
[Section titled “label”](#label)
Text displayed above the recorder. Default: `"Record audio"`. Use an empty string to hide the label.
### `max_duration`
[Section titled “max\_duration”](#max_duration)
Maximum recording length from 1 to 300 seconds. Default: `60`.
### `position`
[Section titled “position”](#position)
Mercury layout placement: `"inline"`, `"sidebar"`, or `"bottom"`. Default: `"inline"`.
### `disabled`
[Section titled “disabled”](#disabled)
Disable the controls and stop an active recording without saving it. Default: `False`.
### `hidden`
[Section titled “hidden”](#hidden)
Hide the widget and stop an active recording without saving it. Default: `False`.
### `key`
[Section titled “key”](#key)
Stable identifier used to reuse the widget across reactive cell executions.
## Permissions and privacy
[Section titled “Permissions and privacy”](#permissions-and-privacy)
The microphone is never activated automatically. Access is requested only after the user selects **Start recording**. Browsers permit microphone access only from an HTTPS deployment or `localhost`, and the user must grant permission for the app’s origin.
Audio remains in the browser while recording. Only the completed recording is sent through the notebook widget connection. Microphone tracks are stopped after recording, or when the widget is hidden, disabled, or removed.
## Theme configuration
[Section titled “Theme configuration”](#theme-configuration)
The recorder uses Mercury theme values loaded from `config.toml`, including `widget_background_color`, `text_color`, `primary_color`, `danger_color`, `border_color`, `border_radius`, and `border_radius_lg`.
# MultiSelect
> How to use the MultiSelect widget in the interactive Mercury App
The **MultiSelect** widget displays a dropdown menu that allows users to choose **multiple values** from a list. It is well suited for filtering data, selecting multiple categories, or controlling more complex logic in your Mercury App.
## Live Demo
[Section titled “Live Demo”](#live-demo)
You can try the `MultiSelect` widget directly in this interactive example:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/input-widgets.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/input-widgets?no-navbar)
## Usage
[Section titled “Usage”](#usage)
To create a MultiSelect widget, you must provide a non-empty list of `choices`. You may optionally provide a list of default selected values using the `value` argument.
If no `value` is provided (or if it is `None`), the widget selects the **first element from `choices` by default**.
### Basic Example
[Section titled “Basic Example”](#basic-example)
Create `MultiSelect` widget:
```python
fruits = MultiSelect(
label="Choose fruits",
choices=["Apple", "Banana", "Cherry"]
)
```
To get the current selection, use `value` attribute:
```python
fruits.value
```
In this case, the initial value will be:
```python
["Apple"]
```
### Default Values
[Section titled “Default Values”](#default-values)
You can specify one or more default selections using the `value` argument.
**Code**
```python
fruits = mr.MultiSelect(
label="Choose fruits",
choices=["Apple", "Banana", "Cherry"],
value=["Banana", "Cherry"]
)
```
If any values provided in `value` are not present in `choices`, they are automatically removed and a warning is shown. If all provided values are invalid, the widget falls back to selecting the **first available choice** during initialization.
### Layout
[Section titled “Layout”](#layout)
Use the `position` argument to control where the widget is displayed. The default is `position="sidebar"`, which renders the widget in the left sidebar.
Other available values are:
* `"inline"` — displays the widget in the main notebook output,
* `"bottom"` — displays the widget after all notebook cells.
**Code**
```python
countries = MultiSelect(
label="Countries",
choices=["USA", "Poland", "Japan"],
position="inline"
)
```
## MultiSelect Props
[Section titled “MultiSelect Props”](#multiselect-props)
### label
[Section titled “label”](#label)
**type:** `string` Text displayed above the widget. The default is `"Select"`.
***
### value
[Section titled “value”](#value)
**type:** `list[str] | None`
Initial list of selected values.
* If `None` or omitted, the first element from `choices` is selected.
* Invalid values are removed during initialization.
* After user interaction, the value may become an **empty list**.
The widget value is **never `None` at runtime**.
***
### choices (required)
[Section titled “choices (required)”](#choices-required)
**type:** `list[str]`
List of available options. Must be non-empty, otherwise an exception is raised.
***
### placeholder
[Section titled “placeholder”](#placeholder)
**type:** `string`
Text displayed when no values are selected. This is visible, for example, after the user clears all selections.
The default is an empty string.
***
### url\_key
[Section titled “url\_key”](#url_key)
**type:** `string`
Name of the URL query parameter used to override the initial value.
* Use repeated query parameters to provide multiple values.
* Missing, empty, or invalid URL values fall back to `value`.
* Matching against `choices` is case-insensitive and preserves the original values from `choices`.
Example:
```text
?fruit=apple&fruit=banana
```
***
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the widget is rendered:
* **sidebar** — in the sidebar (default)
* **inline** — directly in the notebook cell output
* **bottom** — after all notebook cells
***
### disabled
[Section titled “disabled”](#disabled)
**type:** `bool`
If `True`, the widget is visible but cannot be interacted with. The default is `False`.
***
### hidden
[Section titled “hidden”](#hidden)
**type:** `bool`
If `True`, the widget exists in the UI state but is not rendered.
This is useful for internal state management. The default is `False`.
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to distinguish widgets with identical arguments.
Note
If the same code cell is run again, Mercury reuses the previous widget instance unless a different `key` is specified.
The `key` value is required when widgets are created inside loops.
***
## Notes
[Section titled “Notes”](#notes)
* The widget value is always a **list of strings**.
* During initialization, at least one value is selected by default.
* During user interaction, the value may become an **empty list** (for example when the user clears all selections).
* Invalid values are removed automatically.
* `url_key` can be used to initialize the widget from repeated URL query parameters.
* URL values are filtered against `choices`, matched case-insensitively, and deduplicated after matching.
* The current selection is always available via `.value`.
# NumberInput
> How to use the NumberInput widget in the interactive Mercury App
The **NumberInput** widget displays a numeric input field that lets users type a number. It is useful for setting parameters like thresholds, limits, counts, or any value that should be a number.
## Live Demo
[Section titled “Live Demo”](#live-demo)
You can try the `NumberInput` widget directly in this interactive example:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/input-widgets.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/input-widgets?no-navbar)
## Usage
[Section titled “Usage”](#usage)
To create a NumberInput widget, provide a `label`. You can optionally set `value`, `min`, `max`, and `step`.
### Basic Example
[Section titled “Basic Example”](#basic-example)
**Code**
```python
import mercury as mr
n = mr.NumberInput(
label="Enter number",
min=0,
max=100,
step=1
)
```
To get the current value:
```python
n.value
```
***
### Default Value
[Section titled “Default Value”](#default-value)
If `value` is not provided (or is `None`), the widget defaults to `min`.
**Code**
```python
n = mr.NumberInput(
label="Rows",
min=10,
max=100
)
n.value
```
**Output**
```text
10
```
***
### Range and Clamping
[Section titled “Range and Clamping”](#range-and-clamping)
The value is always kept within `[min, max]`.
* If `min > max`, the values are swapped (and a warning is shown).
* If `value` is outside the range, it is clamped to the nearest valid value.
**Code**
```python
n = mr.NumberInput(
label="Probability",
value=2.0,
min=0.0,
max=1.0,
step=0.1
)
n.value
```
**Output**
```text
1.0
```
***
### Layout
[Section titled “Layout”](#layout)
Use the `position` argument to control where the widget is displayed. The default is `position="sidebar"`.
Available positions:
* `"sidebar"` — displayed in the left sidebar (default)
* `"inline"` — displayed in the main notebook output
* `"bottom"` — displayed after all notebook cells
**Code**
```python
mr.NumberInput(
label="Threshold",
value=0.5,
min=0.0,
max=1.0,
step=0.05,
position="inline"
)
```
***
## NumberInput Props
[Section titled “NumberInput Props”](#numberinput-props)
### label
[Section titled “label”](#label)
**type:** `string`
Text displayed above the input. The default is `"Enter number"`.
***
### value
[Section titled “value”](#value)
**type:** `float | None`
Initial numeric value.
* If `None` or omitted, it defaults to `min`.
* If outside `[min, max]`, it is clamped during initialization.
At runtime, `.value` is always a number.
***
### min
[Section titled “min”](#min)
**type:** `float`
Minimum allowed value. The default is `0`.
***
### max
[Section titled “max”](#max)
**type:** `float`
Maximum allowed value. The default is `100`.
***
### step
[Section titled “step”](#step)
**type:** `float`
Step used by the browser numeric input. The default is `1`.
***
### url\_key
[Section titled “url\_key”](#url_key)
**type:** `string`
Name of the URL query parameter used to override the initial value.
* If the URL contains a valid numeric value for this key, it takes precedence over `value`.
* Missing, empty, or invalid URL values fall back to `value`.
* URL values are clamped to `[min, max]` and snapped to the nearest `step`.
Example:
```text
?threshold=1.25
```
***
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the widget is rendered:
* **sidebar** — in the sidebar (default)
* **inline** — directly in the notebook cell output
* **bottom** — after all notebook cells
***
### disabled
[Section titled “disabled”](#disabled)
**type:** `bool`
If `True`, the input is visible but cannot be changed. The default is `False`.
***
### hidden
[Section titled “hidden”](#hidden)
**type:** `bool`
If `True`, the widget exists in the UI state but is not rendered. The default is `False`.
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to distinguish widgets with identical arguments.
Note
If the same code cell is run again, Mercury reuses the previous widget instance unless a different `key` is specified.
The `key` value is required when widgets are created inside loops.
***
## Notes
[Section titled “Notes”](#notes)
* The widget value is always numeric and available via `.value`.
* The value is clamped to the `[min, max]` range.
* `url_key` can be used to initialize the widget from URL query parameters.
* URL values are parsed as numbers, clamped to `[min, max]`, and snapped to the nearest `step`.
* If you need integer-only input, set `step=1` and use integer `min/max`.
# Select
> How to use the Select widget in the interactive Mercury App
The **Select** widget displays a dropdown menu that lets users choose a single value from a list. It is ideal for filtering data, configuring options, or controlling the logic of your Mercury App.
## Live Demo
[Section titled “Live Demo”](#live-demo)
You can try the `Select` widget directly in this interactive example:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/input-widgets.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/input-widgets?no-navbar)
## Usage
[Section titled “Usage”](#usage)
To create a Select widget, you only need to provide a list of choices. If no `value` is given, the first item from the list is selected automatically.
### Basic Example
[Section titled “Basic Example”](#basic-example)
**Code**
```python
import mercury as mr
fruit = mr.Select(
label="Choose fruit",
choices=["Apple", "Banana", "Cherry"]
)
```
To get the current selection:
```python
fruit.value
```
### Layout
[Section titled “Layout”](#layout)
Use `position` argument to change the widget placement. The default is `position="sidebar"` and slider is displayed in the left sidebar. Other available `position` are:
* `"inline"` which displays Slider widget in the main view,
* `"bottom"` which displays Slider widget in the bottom part of main view.
**Code**
```python
sidebar_select = Select(
label="Country",
choices=["USA", "Poland", "Japan"],
position="inline"
)
```
***
## Select Props
[Section titled “Select Props”](#select-props)
### label
[Section titled “label”](#label)
**type:** `string` Text displayed above the dropdown. The deafult is `"Select"`.
***
### value
[Section titled “value”](#value)
**type:** `string` The initial selected option. If omitted or invalid, it defaults to the first element of `choices`.
***
### choices (required)
[Section titled “choices (required)”](#choices-required)
**type:** `list[str]` List of available options. If empty, the widget raises an exception.
***
### url\_key
[Section titled “url\_key”](#url_key)
**type:** `string`
Name of the URL query parameter used to override the initial value.
* If the URL contains a valid value for this key, it takes precedence over `value`.
* Missing, empty, or invalid URL values fall back to `value`.
* Matching against `choices` is case-insensitive and preserves the original value from `choices`.
Example:
```text
?fruit=banana
```
***
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the widget is rendered:
* **sidebar** — in the sidebar (default)
* **inline** — directly in the notebook cell output
* **bottom** — after all notebook cells
***
### disabled
[Section titled “disabled”](#disabled)
**type:** `bool` If `True`, the dropdown is visible but cannot be changed. The default is `False`.
***
### hidden
[Section titled “hidden”](#hidden)
**type:** `bool` If `True`, the widget exists in the UI state but is not rendered. The default is `False`.
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier to distinguish widgets with identical arguments.
Note
If the same code cell is run again, Mercury reuses the previous widget instance unless a different `key` is specified.
The `key` value is needed if widgets are created in the loop.
***
## Notes
[Section titled “Notes”](#notes)
* When no `value` is provided, the widget selects the **first choice**.
* If the provided `value` is not in `choices`, it is replaced with the first element and a warning is shown.
* `url_key` can be used to initialize the widget from URL query parameters.
* URL values are matched against `choices` case-insensitively and use the canonical value from `choices`.
* The current selection is always available via `.value`.
# Slider
> How to use the Slider widget in the interactive Mercury App
The **Slider** widget allows users to select a **numeric value from a range** by dragging a slider handle. It is commonly used for thresholds, limits, percentages, and other numeric parameters in a Mercury App.
## Live Demo
[Section titled “Live Demo”](#live-demo)
You can try the `Slider` widget directly in this interactive example:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/input-widgets.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/input-widgets?no-navbar)
## Usage
[Section titled “Usage”](#usage)
To create a Slider widget, you define the numeric range using `min` and `max`. You can optionally provide an initial `value`.
If no `value` is provided, the slider **defaults to the minimum value**.
### Basic Example
[Section titled “Basic Example”](#basic-example)
**Code**
```python
import mercury as mr
threshold = mr.Slider(
label="Threshold",
min=0,
max=10
)
```
In this example, the initial value will be:
```python
threshold.value
>>> 0
```
### Default Value
[Section titled “Default Value”](#default-value)
You can explicitly define the initial slider position using the `value` argument.
**Code**
```python
threshold = Slider(
label="Threshold",
min=0,
max=10,
value=3
)
```
If the provided value is outside the `[min, max]` range, it is **automatically clamped**.
### Layout
[Section titled “Layout”](#layout)
Use the `position` argument to control where the widget is displayed. The default is `position="sidebar"`, which renders the widget in the left sidebar.
Other available values are:
* `"inline"` — displays the widget directly in the notebook output,
* `"bottom"` — displays the widget after all notebook cells.
**Code**
```python
alpha = Slider(
label="Alpha",
min=0,
max=100,
position="inline"
)
```
## Slider Props
[Section titled “Slider Props”](#slider-props)
### label
[Section titled “label”](#label)
**type:** `string` Text displayed above the slider. The default is `"Select number"`.
***
### value
[Section titled “value”](#value)
**type:** `int | None`
Initial slider value.
* If `None` or omitted, the value defaults to `min`.
* If the value is outside the `[min, max]` range, it is clamped automatically.
The value is always an integer.
***
### min
[Section titled “min”](#min)
**type:** `int`
Minimum allowed value. The default is `0`.
***
### max
[Section titled “max”](#max)
**type:** `int`
Maximum allowed value. The default is `100`.
***
### url\_key
[Section titled “url\_key”](#url_key)
**type:** `string`
Name of the URL query parameter used to override the initial value.
* If the URL contains a valid integer value for this key, it takes precedence over `value`.
* Missing, empty, or invalid URL values fall back to `value`.
* URL values outside the allowed range are clamped to `[min, max]`.
Example:
```text
?threshold=7
```
***
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the widget is rendered:
* **sidebar** — in the sidebar (default)
* **inline** — directly in the notebook cell output
* **bottom** — after all notebook cells
***
### disabled
[Section titled “disabled”](#disabled)
**type:** `bool`
If `True`, the slider is visible but cannot be interacted with. The default is `False`.
***
### hidden
[Section titled “hidden”](#hidden)
**type:** `bool`
If `True`, the widget exists in the UI state but is not rendered.
This is useful for internal state management. The default is `False`.
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to distinguish widgets with identical arguments.
Note
If the same code cell is run again, Mercury reuses the previous widget instance unless a different `key` is specified.
The `key` value is required when widgets are created inside loops.
***
## Behavior Notes
[Section titled “Behavior Notes”](#behavior-notes)
* The slider value is always an **integer**.
* If no value is provided, the slider starts at `min`.
* Values outside the `[min, max]` range are clamped automatically.
* `url_key` can be used to initialize the widget from URL query parameters.
* URL values must be integers and are clamped to `[min, max]`.
* The current value is always available via `.value`.
* The slider track length remains constant, regardless of how many digits the value has.
***
# TextInput
> How to use the TextInput widget in the interactive Mercury App
The **TextInput** widget displays a text input field. It is useful for entering names, labels, filters, search queries, or any short text values in your Mercury App. By default it is a single-line input; set `rows` to a value greater than `1` to get a multi-line textarea.
## Live Demo
[Section titled “Live Demo”](#live-demo)
You can try the `TextInput` widget directly in this interactive example:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/input-widgets.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/input-widgets?no-navbar)
## Usage
[Section titled “Usage”](#usage)
To create a TextInput widget, provide a `label`. The current text entered by the user is always available via `.value`.
### Basic Example
[Section titled “Basic Example”](#basic-example)
**Code**
```python
import mercury as mr
text = mr.TextInput(
label="Enter your name"
)
```
To get the current value:
```python
text.value
```
***
### Default Value
[Section titled “Default Value”](#default-value)
You can provide an initial value using the `value` argument.
**Code**
```python
text = mr.TextInput(
label="City",
value="Warsaw"
)
text.value
```
**Output**
```text
Warsaw
```
***
### Multi-line Text
[Section titled “Multi-line Text”](#multi-line-text)
Set `rows` to a value greater than `1` to render the widget as a resizable textarea.
**Code**
```python
notes = mr.TextInput(
label="Notes",
rows=5
)
notes.value
```
***
### Layout
[Section titled “Layout”](#layout)
Use the `position` argument to control where the widget is displayed. The default is `position="sidebar"`.
Available positions:
* `"sidebar"` — displayed in the left sidebar (default)
* `"inline"` — displayed in the main notebook output
* `"bottom"` — displayed after all notebook cells
**Code**
```python
mr.TextInput(
label="Search",
position="inline"
)
```
***
## TextInput Props
[Section titled “TextInput Props”](#textinput-props)
### label
[Section titled “label”](#label)
**type:** `string`
Text displayed above the input field. The default is `"Enter text"`.
***
### value
[Section titled “value”](#value)
**type:** `string`
Initial text value.
* If omitted, defaults to an empty string.
* The value always reflects the current text entered by the user.
***
### url\_key
[Section titled “url\_key”](#url_key)
**type:** `string`
Name of the URL query parameter used to override the initial value.
* If the URL contains a non-empty value for this key, it takes precedence over `value`.
* Missing, empty, or whitespace-only URL values fall back to `value`.
Example:
```text
?username=jan
```
***
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the widget is rendered:
* **sidebar** — in the sidebar (default)
* **inline** — directly in the notebook cell output
* **bottom** — after all notebook cells
***
### disabled
[Section titled “disabled”](#disabled)
**type:** `bool`
If `True`, the input is visible but cannot be edited. The default is `False`.
***
### hidden
[Section titled “hidden”](#hidden)
**type:** `bool`
If `True`, the widget exists in the UI state but is not rendered. The default is `False`.
***
### rows
[Section titled “rows”](#rows)
**type:** `int`
Number of visible text rows. The default is `1`.
* `rows=1` renders a single-line input field.
* `rows` greater than `1` renders a resizable textarea with that many visible lines.
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to distinguish widgets with identical arguments.
Note
If the same code cell is run again, Mercury reuses the previous widget instance unless a different `key` is specified.
The `key` value is required when widgets are created inside loops.
***
## Notes
[Section titled “Notes”](#notes)
* The widget value is always available via `.value`.
* Changes are debounced slightly to avoid excessive updates.
* `url_key` can be used to initialize the widget from URL query parameters.
* Empty or whitespace-only URL values are ignored.
* For longer text, set `rows` to a value greater than `1` to get a resizable textarea.
# TimeInput
> How to use the TimeInput widget in the interactive Mercury App
The **TimeInput** widget displays a native browser time picker. Its value is available as `HH:MM` or `HH:MM:SS`.
## Usage
[Section titled “Usage”](#usage)
```python
import mercury as mr
time = mr.TimeInput(
label="Select time",
value="14:30",
step=60
)
time.value
```
## TimeInput Props
[Section titled “TimeInput Props”](#timeinput-props)
### label
[Section titled “label”](#label)
**type:** `string`
Text displayed above the input. The default is `"Time"`.
### value
[Section titled “value”](#value)
**type:** `string`
Initial time value in `HH:MM` or `HH:MM:SS` format.
### min
[Section titled “min”](#min)
**type:** `string`
Minimum allowed time.
### max
[Section titled “max”](#max)
**type:** `string`
Maximum allowed time.
### step
[Section titled “step”](#step)
**type:** `int`
Input step in seconds. The default is `60`.
### url\_key
[Section titled “url\_key”](#url_key)
**type:** `string`
URL query parameter name used to override the initial value.
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the widget is rendered. The default is `"sidebar"`.
### disabled
[Section titled “disabled”](#disabled)
**type:** `bool`
If `True`, the input is visible but cannot be edited.
### hidden
[Section titled “hidden”](#hidden)
**type:** `bool`
If `True`, the widget exists in app state but is not rendered.
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to distinguish widgets with identical arguments.
# UploadFile
> How to upload files in the interactive Mercury App
The **UploadFile** widget lets users upload one or more files using **drag & drop** or a **file picker**. Uploaded files are immediately available in Python as byte data, making it easy to load datasets, images, or documents in your Mercury App.
## Live Demo
[Section titled “Live Demo”](#live-demo)
You can try the `UploadFile` widget directly in this interactive example:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/input-widgets.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/input-widgets?no-navbar)
## Usage
[Section titled “Usage”](#usage)
To create an UploadFile widget, provide a `label`. The uploaded content is available via `.value`, `.name`, `.files`, `.names`, or `.values_bytes`.
### Basic Example
[Section titled “Basic Example”](#basic-example)
**Code**
```python
import mercury as mr
uploader = mr.UploadFile(
label="Upload CSV"
)
```
To access the uploaded file:
```python
uploader.name
uploader.value
```
* `name` → file name
* `value` → file content as `bytes`
With the default `multiple=False`, uploading another file replaces the previous one.
***
### Multiple Files
[Section titled “Multiple Files”](#multiple-files)
Enable uploading multiple files using `multiple=True`. When `multiple=False`, the widget stores one file and a new upload replaces the previous file. When `multiple=True`, new uploads are appended to the current file list until the user removes them.
**Code**
```python
uploader = mr.UploadFile(
label="Upload images",
multiple=True
)
```
Iterate over uploaded files:
```python
for f in uploader:
print(f.name, len(f.value))
```
You can also access all files directly:
```python
uploader.files
uploader.names
uploader.values_bytes
```
***
### File Size Limit
[Section titled “File Size Limit”](#file-size-limit)
Use `max_file_size` to limit the size of each uploaded file. The limit is checked in the browser before the file content is synced to Python.
The value must be a positive integer followed by a supported unit:
* `KB`
* `MB`
* `GB`
**Code**
```python
uploader = mr.UploadFile(
label="Upload dataset",
max_file_size="10MB"
)
```
If a file exceeds the limit, the user is notified and the file is not uploaded. Units are case-insensitive, so values like `"10mb"` are normalized to `"10MB"`.
Note
`max_file_size` is enforced by the browser widget before file bytes are synced to Python. It is a per-file limit, not a combined total limit for all uploaded files.
***
### Accepted File Types
[Section titled “Accepted File Types”](#accepted-file-types)
Use `accept` to restrict which file types a user can upload. The restriction is enforced at three levels: the browser file picker, drag-and-drop validation, and a final server-side check before the file reaches Python.
**Code**
```python
uploader = mr.UploadFile(
label="Upload dataset",
accept=".csv"
)
```
Pass a list to allow multiple types:
```python
uploader = mr.UploadFile(
label="Upload data",
accept=[".csv", ".tsv"]
)
```
You can also use MIME types or wildcards:
```python
mr.UploadFile(accept="text/csv") # exact MIME
mr.UploadFile(accept="image/*") # any image
mr.UploadFile(accept=".csv,text/csv") # extension or MIME
```
* `".csv"` — file extension match (recommended for text formats)
* `"text/csv"` — exact MIME type
* `"image/*"` — MIME wildcard
If a file does not match, the upload is blocked and the user receives an alert. The widget also displays a helper text below the upload area, for example *Accepted types: .csv,.tsv*.
Note
Browser-reported MIME types can be empty or unreliable for some formats. For CSV, TSV, and similar files, use the file extension (`.csv`) rather than `"text/csv"` for more consistent validation.
***
### Layout
[Section titled “Layout”](#layout)
Use the `position` argument to control where the widget is displayed. The default is `position="sidebar"`.
Available positions:
* `"sidebar"` — displayed in the left sidebar (default)
* `"inline"` — displayed in the main notebook output
* `"bottom"` — displayed after all notebook cells
**Code**
```python
mr.UploadFile(
label="Upload report",
position="inline"
)
```
***
## UploadFile Props
[Section titled “UploadFile Props”](#uploadfile-props)
### label
[Section titled “label”](#label)
**type:** `string`
Text displayed above the upload area. The default is `"Upload file"`.
***
### max\_file\_size
[Section titled “max\_file\_size”](#max_file_size)
**type:** `string`
Maximum allowed size per file. The value must be a positive integer followed by `KB`, `MB`, or `GB`.
Examples:
* `"500KB"`
* `"10MB"`
* `"1GB"`
The default is `"100MB"`. The unit can be written as `KB`, `MB`, or `GB` and is case-insensitive.
***
### multiple
[Section titled “multiple”](#multiple)
**type:** `bool`
If `True`, allows uploading multiple files. The default is `False`.
With `multiple=False`, selecting or dropping a new file replaces the previous file. With `multiple=True`, selecting or dropping files appends them to the current list.
***
### accept
[Section titled “accept”](#accept)
**type:** `string`
Restricts which file types can be uploaded. The default is `""` (all file types allowed).
Supported formats:
* File extension: `".csv"`, `".png"`
* Exact MIME type: `"text/csv"`, `"application/json"`
* MIME wildcard: `"image/*"`, `"video/*"`
Pass a list or a comma-separated string to allow multiple types: `[".csv", ".tsv"]` or `".csv,.tsv"`. Invalid values raise a `ValueError`. When `multiple=True`, each file is validated independently.
***
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the widget is rendered:
* **sidebar** — in the sidebar (default)
* **inline** — directly in the notebook cell output
* **bottom** — after all notebook cells
***
### disabled
[Section titled “disabled”](#disabled)
**type:** `bool`
If `True`, the widget is visible but cannot be interacted with. The default is `False`.
***
### hidden
[Section titled “hidden”](#hidden)
**type:** `bool`
If `True`, the widget exists in the UI state but is not rendered. The default is `False`.
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to distinguish widgets with identical arguments.
Note
If the same code cell is run again, Mercury reuses the previous widget instance unless a different `key` is specified.
The `key` value is required when widgets are created inside loops.
***
## Notes
[Section titled “Notes”](#notes)
* Uploaded files are stored **in memory**, not saved to disk automatically.
* `max_file_size` is a **per-file** browser-side limit.
* Each uploaded file is represented as an `UploadedFile` object with:
* `.name` — file name
* `.value` — file content as `bytes`
* For `multiple=False`, `.value` and `.name` refer to the **first file**.
* For `multiple=True`, use `.files`, `.names`, `.values_bytes`, or iteration to handle all uploads.
* `accept` is enforced in the browser file picker, on drag-and-drop, and again server-side before the file reaches Python. For text formats like CSV, prefer `.csv` over `"text/csv"`.
# Installation
> Install Mercury framework
Mercury is available as Python package. It can be easily installed with the following command:
Install Command
```bash
pip install mercury
```
## What is included?
[Section titled “What is included?”](#what-is-included)
After installation you will have access to:
* collection of interactive widgets,
* Mercury Server - application that serves your notebooks as web app.
* Mercury extension that allows you to live preview web app during notebook development side by side.
Note
Live app preview feature is available only in **MLJAR Studio** and **JupyterLab** editors.
## Uninstall
[Section titled “Uninstall”](#uninstall)
To remove `mercury` package from your environment please execute the following command:
Uninstall Command
```bash
pip install mercury
```
## Development install
[Section titled “Development install”](#development-install)
Please check `CONTRIBUTING.md` file in the GitHub repository for development install instructions.
# Columns
> How to create responsive column layouts in a Mercury App
The **Columns** helper lets you create a responsive row of output areas (columns) inside a Mercury App. It is useful for building dashboards, side-by-side comparisons, charts grids, or multi-panel layouts.
Each column is returned as an `Output` widget, so you can write into it using a `with` block. On rerun, reused column outputs are cleared by default so the layout shows the latest execution result.
## Live Demo
[Section titled “Live Demo”](#live-demo)
You can check the `Columns` widget directly in this interactive example:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/layout-widgets.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/layout-widgets?no-navbar)
## Usage
[Section titled “Usage”](#usage)
Call `mr.Columns()` to create a row of columns. The function returns a **tuple of output widgets** — one for each column. Columns are responsive by default: they stay side by side while each column has comfortable space and automatically stack as their containing area gets narrower.
### Basic Example
[Section titled “Basic Example”](#basic-example)
**Code**
```python
import mercury as mr
col1, col2 = mr.Columns(2)
with col1:
print("Left column")
with col2:
print("Right column")
```
### Three Columns
[Section titled “Three Columns”](#three-columns)
**Code**
```python
c1, c2, c3 = mr.Columns(3)
with c1:
print("Column 1")
with c2:
print("Column 2")
with c3:
print("Column 3")
```
### Proportional Widths
[Section titled “Proportional Widths”](#proportional-widths)
Pass a list of positive numbers to create columns with proportional widths. For example, `[0.4, 0.6]` creates two columns where the first takes about 40% and the second takes about 60% of the available row space.
**Code**
```python
left, right = mr.Columns([0.4, 0.6])
with left:
print("40% column")
with right:
print("60% column")
```
The values are treated as ratios, so `[40, 60]`, `[0.4, 0.6]`, and `[2, 3]` all work.
### Minimum Width and Responsiveness
[Section titled “Minimum Width and Responsiveness”](#minimum-width-and-responsiveness)
No responsive configuration is required. By default, each column targets a minimum width of 260px. Two columns therefore stay in one row on desktop and tablet layouts, then stack into two rows on a typical mobile screen. Four columns can progressively wrap from four to two to one per row as their containing area gets narrower.
Responsiveness is based on the available container width, so it also works for nested layouts and pages with a sidebar.
Use `min_width` only when you want columns to wrap earlier or allow a denser layout.
**Code**
```python
c1, c2, c3 = mr.Columns(
n=3,
min_width="320px"
)
```
### Gap Between Columns
[Section titled “Gap Between Columns”](#gap-between-columns)
Control spacing between columns using the `gap` argument.
**Code**
```python
c1, c2 = mr.Columns(
n=2,
gap="32px"
)
```
### Borders
[Section titled “Borders”](#borders)
You can control column borders explicitly or let the theme decide.
**Examples**
```python
# Theme-based borders (default)
mr.Columns(2)
# Custom border
mr.Columns(2, border="1px solid lightgray")
# No borders
mr.Columns(2, border="")
```
### Layout Position
[Section titled “Layout Position”](#layout-position)
Use the `position` argument to control where the columns are displayed. The default is `position="inline"`.
Available positions:
* `"sidebar"` — display columns in the sidebar
* `"inline"` — display columns in the main notebook output (default)
* `"bottom"` — display columns after all notebook cells
**Code**
```python
mr.Columns(
n=2,
position="bottom"
)
```
### Append Previous Content
[Section titled “Append Previous Content”](#append-previous-content)
By default, `append=False`, so content from the previous run is cleared before new content is written. Set `append=True` when you intentionally want to keep previous output and append new content on each run.
**Code**
```python
col1, col2 = mr.Columns(2, append=True)
with col1:
print("This output is appended on each run")
```
### Clear Content Manually
[Section titled “Clear Content Manually”](#clear-content-manually)
Each returned column is an output widget and supports `.clear()`. Use it when you want to clear a column manually from your notebook code.
**Code**
```python
col1, col2 = mr.Columns(2)
col1.clear()
with col1:
print("Fresh content")
```
## Columns Props
[Section titled “Columns Props”](#columns-props)
### n
[Section titled “n”](#n)
**type:** `int | list[float]`
Number of equal-width columns, or a list of proportional widths.
Examples:
* `2` — two equal-width columns
* `[0.4, 0.6]` — two columns with a 40/60 width ratio
* `[1, 2, 1]` — three columns with a 1/2/1 width ratio
Integer values must be at least 1. Width-list values must be positive numbers.
***
### min\_width
[Section titled “min\_width”](#min_width)
**type:** `string`
Minimum width of each column (CSS value). The default targets `260px` without allowing a column to overflow a narrower parent.
Examples:
* `"120px"`
* `"240px"`
***
### gap
[Section titled “gap”](#gap)
**type:** `string`
Gap between columns (CSS value).
Examples:
* `"8px"`
* `"16px"`
* `"32px"`
***
### border
[Section titled “border”](#border)
**type:** `string | None`
Controls column borders:
* `None` — use theme defaults
* `""` — disable borders
* CSS string — custom border (e.g. `"1px solid red"`)
***
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the columns are rendered.
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to distinguish widgets with identical arguments.
Note
If the same code cell is run again, Mercury reuses the previous Columns instance unless a different `key` is specified.
The `key` value is required when Columns are created inside loops.
***
### append
[Section titled “append”](#append)
**type:** `bool`
Controls what happens when Mercury reuses the same Columns instance:
* `False` — clear previous column content before writing new content (default)
* `True` — keep previous content and append new output
***
## Notes
[Section titled “Notes”](#notes)
* Columns are responsive and automatically wrap when space is limited.
* Each returned object is an `Output` widget with a `.clear()` method.
* Use `with column:` blocks to write content into columns.
* By default, reruns replace previous column content. Use `append=True` to accumulate content.
* Columns are ideal for charts, tables, metrics, or grouped controls.
# Expander
> How to use the Expander helper in the interactive Mercury App
The **Expander** helper lets you hide and show a block of content under a clickable header. It is useful for optional settings, advanced parameters, explanations, or details that should not be visible by default.
`mr.Expander()` returns an output area, so you can write content into it using a `with` block. On rerun, reused expander content is cleared by default so it shows the latest execution result.
## Live Demo
[Section titled “Live Demo”](#live-demo)
You can check the `Expander` widget directly in this interactive example:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/layout-widgets.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/layout-widgets?no-navbar)
## Usage
[Section titled “Usage”](#usage)
Call `mr.Expander()` to create an expander section. The function returns an output widget you can write into.
### Basic Example
[Section titled “Basic Example”](#basic-example)
**Code**
```python
import mercury as mr
details = mr.Expander(label="Details")
with details:
print("Hidden content")
```
### Expanded by Default
[Section titled “Expanded by Default”](#expanded-by-default)
Use `expanded=True` to open the expander on first render.
**Code**
```python
import mercury as mr
details = mr.Expander(
label="Advanced settings",
expanded=True
)
with details:
print("Shown immediately")
```
### Common Pattern: Advanced Options
[Section titled “Common Pattern: Advanced Options”](#common-pattern-advanced-options)
**Code**
```python
import mercury as mr
# Basic controls
name = mr.TextInput(label="Name")
# Advanced section
advanced = mr.Expander(label="Advanced")
with advanced:
threshold = mr.NumberInput(label="Threshold", value=0.5, min=0.0, max=1.0, step=0.05)
refresh = mr.Checkbox(label="Auto-refresh")
```
### Border and Header Background
[Section titled “Border and Header Background”](#border-and-header-background)
By default, the expander has an outer border, a box background, and a shaded clickable header. You can turn off the outer border and header background independently.
Use `show_border=False` and `header_background=False` when you want the expander to blend into the surrounding layout.
**Code**
```python
import mercury as mr
plain = mr.Expander(
label="More filters",
show_border=False,
header_background=False
)
with plain:
print("Borderless content")
```
You can also disable only one part:
```python
# No outer border, but keep the shaded header
mr.Expander("Filters", show_border=False)
# Keep the outer border, but make the header transparent
mr.Expander("Filters", header_background=False)
```
### Append Previous Content
[Section titled “Append Previous Content”](#append-previous-content)
By default, `append=False`, so content from the previous run is cleared before new content is written. Set `append=True` when you intentionally want the expander to accumulate output across runs.
**Code**
```python
details = mr.Expander(label="Run log", append=True)
with details:
print("This line is appended on each run")
```
### Clear Content Manually
[Section titled “Clear Content Manually”](#clear-content-manually)
The returned expander output supports `.clear()`. Use it when you want to clear expander content manually from your notebook code.
**Code**
```python
details = mr.Expander(label="Run log")
details.clear()
with details:
print("Fresh details")
```
## Expander Props
[Section titled “Expander Props”](#expander-props)
### label
[Section titled “label”](#label)
**type:** `string`
Text displayed in the expander header. The default is `"Details"`.
***
### expanded
[Section titled “expanded”](#expanded)
**type:** `bool`
If `True`, the expander starts in the open state. The default is `False`.
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to distinguish expanders with identical arguments.
Note
If the same code cell is run again, Mercury reuses the previous expander instance unless a different `key` is specified.
The `key` value is required when expanders are created inside loops.
***
### show\_border
[Section titled “show\_border”](#show_border)
**type:** `bool`
Controls the outer expander box.
* `True` — show the border and box background (default)
* `False` — remove the outer border and box background
The default is `True`.
***
### header\_background
[Section titled “header\_background”](#header_background)
**type:** `bool`
Controls the clickable header background.
* `True` — use the default shaded header background (default)
* `False` — make the header background transparent
The default is `True`.
***
### append
[Section titled “append”](#append)
**type:** `bool`
Controls what happens when Mercury reuses the same Expander instance:
* `False` — clear previous expander content before writing new content (default)
* `True` — keep previous content and append new output
***
## Notes
[Section titled “Notes”](#notes)
* `mr.Expander()` returns an output area with a `.clear()` method. Put content inside using `with`.
* By default, reruns replace previous expander content. Use `append=True` to accumulate content.
* The expander uses a single unified border and smooth open/close animation.
* Use expanders for optional or advanced settings to keep the UI clean.
# Tabs
> How to use the Tabs helper in the interactive Mercury App
The **Tabs** helper lets you organize content into multiple tabs, showing one panel at a time. It is useful for dashboards, comparisons, multi-step outputs, or grouping related results without cluttering the UI.
`mr.Tabs()` returns a tuple of output areas — one per tab. On rerun, reused tab outputs are cleared by default so each tab shows the latest execution result.
## Live Demo
[Section titled “Live Demo”](#live-demo)
You can check the `Tabs` widget directly in this interactive example:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/layout-widgets.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/layout-widgets?no-navbar)
## Usage
[Section titled “Usage”](#usage)
Call `mr.Tabs()` with a list of tab labels. The function returns one output widget for each tab.
### Basic Example
[Section titled “Basic Example”](#basic-example)
**Code**
```python
import mercury as mr
tabs = mr.Tabs(labels=["Overview", "Details", "Logs"])
with tabs[0]:
print("Overview content")
with tabs[1]:
print("Detailed results")
with tabs[2]:
print("Logs and debug output")
```
### Set Active Tab
[Section titled “Set Active Tab”](#set-active-tab)
Use the `active` argument to select which tab is visible initially.
**Code**
```python
import mercury as mr
tabs = mr.Tabs(
labels=["Train", "Validate", "Test"],
active=1
)
with tabs[1]:
print("Validation results")
```
### Layout Position
[Section titled “Layout Position”](#layout-position)
Use the `position` argument to control where the tabs are rendered. The default is `position="inline"`.
Available values:
* `"sidebar"` — render tabs in the sidebar
* `"inline"` — render tabs in the main notebook flow (default)
* `"bottom"` — render tabs after all notebook cells
**Code**
```python
mr.Tabs(
labels=["Settings", "Preview"],
position="sidebar"
)
```
### Append Previous Content
[Section titled “Append Previous Content”](#append-previous-content)
By default, `append=False`, so content from the previous run is cleared before new content is written. Set `append=True` when you intentionally want a tab to accumulate output across runs.
**Code**
```python
tabs = mr.Tabs(labels=["Events", "Summary"], append=True)
with tabs[0]:
print("This output is appended on each run")
```
### Clear Content Manually
[Section titled “Clear Content Manually”](#clear-content-manually)
Each returned tab output supports `.clear()`. Use it when you want to clear a tab manually from your notebook code.
**Code**
```python
tabs = mr.Tabs(labels=["Events", "Summary"])
tabs[0].clear()
with tabs[0]:
print("Fresh tab content")
```
## Tabs Props
[Section titled “Tabs Props”](#tabs-props)
### labels
[Section titled “labels”](#labels)
**type:** `list[str]`
Labels displayed in the tab header. Each label creates one tab and one output panel.
***
### active
[Section titled “active”](#active)
**type:** `int`
Index of the initially active tab (0-based). The default is `0`.
***
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the tabs container is rendered:
* **sidebar** — in the sidebar
* **inline** — directly in the notebook output (default)
* **bottom** — after all notebook cells
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to distinguish tabs with identical arguments.
Note
If the same code cell is run again, Mercury reuses the previous Tabs instance unless a different `key` is specified.
The `key` value is required when tabs are created inside loops.
***
### append
[Section titled “append”](#append)
**type:** `bool`
Controls what happens when Mercury reuses the same Tabs instance:
* `False` — clear previous tab content before writing new content (default)
* `True` — keep previous content and append new output
***
## Notes
[Section titled “Notes”](#notes)
* `mr.Tabs()` returns a tuple of output widgets with `.clear()` methods.
* Write content into a tab using `with tabs[i]: ...`.
* Only one tab panel is visible at a time.
* By default, reruns replace previous tab content. Use `append=True` to accumulate content.
* Tabs automatically handle keyboard navigation (arrow keys, Home, End).
* Content inside tabs is responsive and constrained to the container width.
# Activity Calendar
> Display prepared daily numeric data as a GitHub-style activity calendar
The `ActivityCalendar` output widget displays one numeric value per calendar day. It fills missing dates with inactive squares and generates lighter intensity shades from one selected color.
`ActivityCalendar` does not aggregate data. Prepare exactly one row per day before passing the DataFrame to Mercury.
## Basic usage
[Section titled “Basic usage”](#basic-usage)
```python
import pandas as pd
import mercury as mr
df = pd.DataFrame({
"date": [
"2026-08-20",
"2026-08-21",
"2026-08-22",
"2026-08-23",
],
"outage_hours": [0.5, 2.1, 0, 4.8],
})
mr.ActivityCalendar(
df,
date="date",
value="outage_hours",
title="GitHub outages",
unit="hours",
)
```
Green is the default activity color. Zero values and days missing from the DataFrame use the inactive theme color.
## Colors and intensity
[Section titled “Colors and intensity”](#colors-and-intensity)
Select a green or red calendar with a single option:
```python
mr.ActivityCalendar(df, date="date", value="outage_hours", color="green")
mr.ActivityCalendar(df, date="date", value="outage_hours", color="red")
```
Mercury treats the selected color as the strongest activity level and automatically generates lighter shades for lower levels. Named colors use the theme success and danger colors, so they follow customized Mercury themes.
You can also provide a custom hex color:
```python
mr.ActivityCalendar(
df,
date="date",
value="outage_hours",
color="#8b5cf6",
)
```
With the default `levels=5`, the scale contains one inactive level and four positive intensity levels. Positive values are placed on a linear scale from zero to the largest value in the displayed date range.
## Date range
[Section titled “Date range”](#date-range)
By default, the calendar starts at the earliest date and ends at the latest date in the DataFrame. Extend or limit the displayed range with inclusive boundaries:
```python
mr.ActivityCalendar(
df,
date="date",
value="outage_hours",
start_date="2026-01-01",
end_date="2026-12-31",
)
```
Missing days inside the range are rendered as inactive squares. A range spanning multiple years is rendered as one calendar per year, using the same intensity scale. Multi-year calendars use matching full-year grids, keeping month labels vertically aligned even when the first or last year contains only part of the requested range.
## Optional labels
[Section titled “Optional labels”](#optional-labels)
Month labels, weekday labels, and the legend are enabled by default. Hide any of them independently:
```python
mr.ActivityCalendar(
df,
date="date",
value="outage_hours",
show_months=False,
show_weekdays=False,
show_legend=False,
)
```
Every square includes an accessible label and a native tooltip containing the date, value, and optional unit. Calendars keep fixed-size squares and scroll horizontally inside narrow layouts such as `mr.Columns(2)`.
## Parameters
[Section titled “Parameters”](#parameters)
| Parameter | Description | Default |
| --------------- | ------------------------------------------------------- | ------------------ |
| `data` | A non-empty pandas DataFrame with one row per day | required |
| `date` | Date column name | `"date"` |
| `value` | Numeric value column name | `"value"` |
| `title` | Optional heading | `None` |
| `unit` | Unit shown in tooltips and near the legend | `None` |
| `color` | `"green"`, `"red"`, or a hex color | green |
| `start_date` | Inclusive first displayed day | earliest data date |
| `end_date` | Inclusive last displayed day | latest data date |
| `levels` | Total intensity levels, including inactive; minimum `2` | `5` |
| `show_legend` | Display the Less–More legend | `True` |
| `show_weekdays` | Display weekday labels | `True` |
| `show_months` | Display month labels | `True` |
Duplicate normalized dates raise an error. Aggregate duplicate dates yourself before constructing the calendar so the widget never silently sums or averages user data.
# Download
> How to add a download button for files and data with the Download widget in a Mercury App
The **Download** widget renders a button that lets users download data generated inside a Mercury App.\
It is useful for exporting results, reports, CSV files, model artifacts, or any other content produced during notebook execution.
The widget works entirely in the browser by creating a temporary file (Blob) and triggering a download when the button is clicked.
## Basic Usage
[Section titled “Basic Usage”](#basic-usage)
### Download a Text File
[Section titled “Download a Text File”](#download-a-text-file)
```python
import mercury as mr
mr.Download(
data="Hello world!\nThis file was generated by Mercury.",
filename="hello.txt",
label="Download TXT"
)
```
Clicking the button downloads a file named `hello.txt`.
## Download Common File Types
[Section titled “Download Common File Types”](#download-common-file-types)
### CSV
[Section titled “CSV”](#csv)
```python
import mercury as mr
csv_data = "a,b\n1,2\n3,4\n"
mr.Download(
data=csv_data,
filename="data.csv",
mime="text/csv",
label="Download CSV"
)
```
### JSON
[Section titled “JSON”](#json)
```python
import json
import mercury as mr
payload = {"a": 1, "b": [1, 2, 3]}
mr.Download(
data=json.dumps(payload, indent=2),
filename="data.json",
mime="application/json",
label="Download JSON"
)
```
### Download a pandas DataFrame
[Section titled “Download a pandas DataFrame”](#download-a-pandas-dataframe)
```python
import pandas as pd
import mercury as mr
df = pd.DataFrame({
"name": ["Alice", "Bob", "Charlie"],
"score": [82, 91, 77]
})
csv_data = df.to_csv(index=False)
mr.Download(
data=csv_data,
filename="scores.csv",
mime="text/csv",
label="Download CSV"
)
```
## Binary Files (Base64)
[Section titled “Binary Files (Base64)”](#binary-files-base64)
For binary data (images, models, zip files), encode the content as **base64** and set `is_base64=True`.
```python
import base64
import mercury as mr
raw_bytes = b"\x00\x01\x02\x03"
b64 = base64.b64encode(raw_bytes).decode("ascii")
mr.Download(
data=b64,
filename="data.bin",
mime="application/octet-stream",
is_base64=True,
label="Download binary"
)
```
Note
When `is_base64=True`, decoding happens in the browser before the download starts.
***
## Layout
[Section titled “Layout”](#layout)
Use `position` to control where the download button is displayed:
* `"sidebar"` — place the button in the sidebar (default)
* `"inline"` — render it in the notebook output flow
* `"bottom"` — render it after all notebook cells
```python
import mercury as mr
mr.Download(
data="Report content",
filename="report.txt",
position="bottom"
)
```
***
## Download Props
[Section titled “Download Props”](#download-props)
### data (required)
[Section titled “data (required)”](#data-required)
**type:** `string | bytes`
Content of the file.
* Plain text by default
* Base64-encoded string if `is_base64=True`
***
### filename
[Section titled “filename”](#filename)
**type:** `string`
Name of the downloaded file.
Default: `"file.txt"`
***
### label
[Section titled “label”](#label)
**type:** `string`
Text displayed on the download button.
Default: `"Download"`
***
### mime
[Section titled “mime”](#mime)
**type:** `string`
MIME type of the file.
Default: `"text/plain"`
***
### is\_base64
[Section titled “is\_base64”](#is_base64)
**type:** `bool`
If `True`, `data` is treated as base64-encoded content.
Default: `False`
***
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the widget is rendered.
Default: `"sidebar"`
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to reuse the same widget instance.
***
## Notes
[Section titled “Notes”](#notes)
* `Download` renders immediately and does not require user interaction until the button is clicked.
* Large files may increase browser memory usage since data is held in memory.
* The widget works fully client-side; no server storage is involved.
# Funnel
> Display conversion stages as a responsive funnel chart
The `Funnel` output widget displays an ordered sequence of stages. Each section’s width represents its value, and the percentage shows conversion from either the previous stage or the first stage.
Prepare one aggregated value per stage before constructing the widget. `Funnel` preserves the supplied order and does not sort or aggregate data.
## Basic usage
[Section titled “Basic usage”](#basic-usage)
```python
import pandas as pd
import mercury as mr
df = pd.DataFrame({
"stage": [
"Visitors",
"Signups",
"Trials",
"Customers",
],
"users": [
10000,
3200,
1200,
340,
],
})
mr.Funnel(
df,
stage="stage",
value="users",
)
```
You can also pass two-item tuples or an ordered dictionary:
```python
mr.Funnel([
("Visitors", 10000),
("Signups", 3200),
("Trials", 1200),
("Customers", 340),
])
```
## Percentages
[Section titled “Percentages”](#percentages)
By default, each percentage compares the stage with the previous stage:
```python
mr.Funnel(df, stage="stage", value="users", percentage="previous")
```
Use `percentage="first"` to compare every stage with the first stage:
```python
mr.Funnel(df, stage="stage", value="users", percentage="first")
```
The first stage displays `100%`. When the comparison stage is zero, Mercury displays an em dash instead of NaN or infinity. Increasing values are supported and can produce percentages greater than 100%.
## Customize the funnel
[Section titled “Customize the funnel”](#customize-the-funnel)
```python
mr.Funnel(
df,
stage="stage",
value="users",
height=500,
show_values=True,
show_percentage=True,
percentage="first",
colors=[
"#1d4ed8",
"#2563eb",
"#3b82f6",
"#60a5fa",
],
)
```
`colors` accepts a list that cycles through stages or a dictionary that assigns colors to stage names. When colors are omitted, Mercury generates coordinated shades from the theme’s primary color.
Hide values or percentages independently:
```python
mr.Funnel(df, stage="stage", value="users", show_values=False)
mr.Funnel(df, stage="stage", value="users", show_percentage=False)
```
## Input requirements
[Section titled “Input requirements”](#input-requirements)
Every stage needs a non-empty name and a finite, non-negative numeric value. Zero values are supported. Values do not need to decrease, and stages always remain in input order.
The input should already contain one row per funnel stage:
```text
stage | value
Visitors | 10000
Signups | 3200
Trials | 1200
Customers | 340
```
## Labels, tooltips, and responsive layout
[Section titled “Labels, tooltips, and responsive layout”](#labels-tooltips-and-responsive-layout)
Labels are placed beside the funnel so they remain readable for very small and zero stages. Each section includes a native browser tooltip and an accessible label. The SVG fills the available width and scrolls horizontally only when its container becomes too narrow to keep labels readable.
## Parameters
[Section titled “Parameters”](#parameters)
| Parameter | Description | Default |
| ----------------- | ------------------------------------------------------------------- | ------------ |
| `data` | DataFrame, two-item tuples, row dictionaries, or ordered dictionary | required |
| `stage` | Stage column or row-dictionary key | `"stage"` |
| `value` | Numeric value column or row-dictionary key | `"value"` |
| `colors` | Hex color list, stage-color dictionary, or `None` | `None` |
| `height` | Optional SVG layout height | `None` |
| `show_values` | Display formatted values | `True` |
| `show_percentage` | Display conversion percentages | `True` |
| `percentage` | Compare with `"previous"` or `"first"` stage | `"previous"` |
| `value_format` | Python numeric format specification | `","` |
The implementation is generated entirely in Python and rendered as SVG. It does not load Plotly, D3, or another JavaScript charting library.
# ImageCard
> How to display images from URLs or local files with the ImageCard helper in a Mercury App
The **ImageCard** helper displays an image (from a URL or a local file) inside a styled card, with an optional caption below it.
Note
`ImageCard` displays the widget immediately and does not return a value. Use it like other display helpers in Mercury: call it in a cell and it will render in the UI.
## Basic Usage
[Section titled “Basic Usage”](#basic-usage)
### Example: Image from URL
[Section titled “Example: Image from URL”](#example-image-from-url)
```python
import mercury as mr
mr.ImageCard(
src="https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg",
caption="A cat loaded from a public URL"
)
```
### Example: Image from Local File
[Section titled “Example: Image from Local File”](#example-image-from-local-file)
```python
import mercury as mr
mr.ImageCard(
src="assets/plot.png",
caption="Saved plot from the experiment"
)
```
Note
For local files, `ImageCard` reads the image and embeds it as a `data:` URI, so it can be displayed reliably in the Mercury UI.
## Caption
[Section titled “Caption”](#caption)
Use `caption` to show a centered italic description below the image:
```python
import mercury as mr
mr.ImageCard(
src="assets/result.png",
caption="Training result after 50 epochs"
)
```
If `caption=""` (default), no caption element is rendered.
## Sizing
[Section titled “Sizing”](#sizing)
### Card Width
[Section titled “Card Width”](#card-width)
Use `width` to control the outer card width (CSS value):
```python
import mercury as mr
mr.ImageCard(
src="assets/result.png",
width="420px"
)
```
Default: `"100%"`
### Fixed Image Height
[Section titled “Fixed Image Height”](#fixed-image-height)
Use `height` to create a fixed image area (useful when you want multiple cards aligned). When `height` is provided, the image uses `object-fit: contain`.
```python
import mercury as mr
mr.ImageCard(
src="assets/result.png",
height="240px",
caption="Fixed-height preview"
)
```
Default: `None` (natural image height)
## Styling Options
[Section titled “Styling Options”](#styling-options)
### Rounded Corners
[Section titled “Rounded Corners”](#rounded-corners)
```python
import mercury as mr
mr.ImageCard(
src="assets/result.png",
rounded=False
)
```
Default: `True`
### Border
[Section titled “Border”](#border)
```python
import mercury as mr
mr.ImageCard(
src="assets/result.png",
show_border=False
)
```
Default: `True`
## ImageCard Props
[Section titled “ImageCard Props”](#imagecard-props)
### src (required)
[Section titled “src (required)”](#src-required)
**type:** `string`
Image source. Can be:
* an `http://` or `https://` URL, or
* a local filesystem path (e.g. `assets/plot.png`)
***
### caption
[Section titled “caption”](#caption-1)
**type:** `string`
Optional caption text shown below the image (centered and italic).
Default: `""`
***
### width
[Section titled “width”](#width)
**type:** `string`
CSS width for the outer card (e.g. `"100%"`, `"400px"`).
Default: `"100%"`
***
### height
[Section titled “height”](#height)
**type:** `string | None`
Fixed CSS height for the image area (e.g. `"240px"`). If `None`, the image uses its natural height.
Default: `None`
***
### rounded
[Section titled “rounded”](#rounded)
**type:** `bool`
If `True`, apply theme border radius to the image area.
Default: `True`
***
### show\_border
[Section titled “show\_border”](#show_border)
**type:** `bool`
If `True`, show a border around the card.
Default: `True`
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier to reuse the same widget instance.
***
## Notes
[Section titled “Notes”](#notes)
* `ImageCard` **renders immediately** (it is a display helper) and does not return a value.
* Local files are embedded as `data:` URIs (base64), which makes them easy to render in the UI.
* If `src` is neither a valid URL nor an existing file path, it is used as-is (so invalid paths will likely show a broken image).
* Styling is aligned with the Mercury theme (`THEME`) and is injected globally once per notebook session.
# Indicator
> How to display KPI-style metric cards with the Indicator component in a Mercury App
The **Indicator** component renders a compact KPI-style card for displaying a metric value, an optional label, and an optional delta (change) badge.
It is useful for dashboards and summaries such as:
* number of users
* revenue / costs
* accuracy / AUC / latency
* conversion rate changes
* experiment comparisons
Indicators can be displayed:
* as a **single card**, or
* as a **responsive row of cards** (by passing a list of `Indicator` objects).
## Basic Usage
[Section titled “Basic Usage”](#basic-usage)
### Single Indicator
[Section titled “Single Indicator”](#single-indicator)
```python
import mercury as mr
mr.Indicator(
value="123",
label="Users",
delta=5.4
)
```
* `value` is displayed as the main metric
* `label` is shown above the value
* `delta` shows a directional badge (green/red/neutral) if numeric
## Delta (Change Badge)
[Section titled “Delta (Change Badge)”](#delta-change-badge)
`delta` can be numeric or text:
* `delta > 0` → green badge with ↑
* `delta == 0` → neutral badge, no arrow
* `delta < 0` → red badge with ↓
* non-numeric delta → displayed as-is (no arrow logic)
```python
mr.Indicator(value="98%", label="Accuracy", delta=-1.2)
```
```python
mr.Indicator(value="0%", label="Change", delta=0)
```
```python
mr.Indicator(value="OK", label="Status", delta="stable")
```
Note
The delta badge formats numeric values as a percentage (`abs(delta)%`), with trailing zeros stripped — for example `5.0%` is displayed as `5%`.\
If you want custom formatting, pass `delta` as a string (for example: `"−1.2 pp"`).
## Multiple Indicators in One Row
[Section titled “Multiple Indicators in One Row”](#multiple-indicators-in-one-row)
To render a row of indicators, pass a list of `Indicator` objects as the `value` of another `Indicator`:
```python
import mercury as mr
mr.Indicator([
mr.Indicator(value="123", label="Users", delta=5.4),
mr.Indicator(value="98%", label="Accuracy", delta=-1.2),
mr.Indicator(value="1.3s", label="Latency"),
])
```
The row is responsive:
* on wide screens it displays as a row of cards
* on small screens it switches to a single-column layout
## Variants
[Section titled “Variants”](#variants)
Use the `variant` parameter to apply a predefined color scheme. Each variant sets the accent bar, border, and badge colors automatically. The card background is always white.
### Semantic variants
[Section titled “Semantic variants”](#semantic-variants)
```python
mr.Indicator(value="1,024", label="Users", delta=5.4, variant="primary")
mr.Indicator(value="98%", label="Accuracy", delta=2.1, variant="success")
mr.Indicator(value="340ms", label="Latency", delta=18.0, variant="warning")
mr.Indicator(value="4.2%", label="Error rate", delta=-11.0, variant="danger")
mr.Indicator(value="1.3s", label="P99", variant="neutral")
```
| Variant | Use case |
| --------- | ------------------------------ |
| `primary` | Default, Mercury brand blue |
| `success` | Good metric, target hit |
| `warning` | Needs attention |
| `danger` | Bad metric, threshold breached |
| `neutral` | No signal, structural info |
### Domain variants
[Section titled “Domain variants”](#domain-variants)
```python
mr.Indicator(value="0.94", label="AUC", delta=0.02, variant="ml")
mr.Indicator(value="17", label="Columns", variant="info")
mr.Indicator(value="$12k", label="Revenue", delta=3.1, variant="teal")
mr.Indicator(value="63%", label="CTR", delta=4.0, variant="pink")
mr.Indicator(value="82%", label="CPU", delta=-1.0, variant="orange")
```
| Variant | Use case |
| -------- | --------------------------------- |
| `ml` | Model metrics — AUC, accuracy, F1 |
| `info` | Informational counts |
| `teal` | Financial / revenue |
| `pink` | Marketing / engagement |
| `orange` | Performance / ops |
### Row with variants
[Section titled “Row with variants”](#row-with-variants)
```python
mr.Indicator([
mr.Indicator(value="123", label="Users", delta=5.4, variant="primary"),
mr.Indicator(value="98%", label="Accuracy", delta=-1.2, variant="danger"),
mr.Indicator(value="1.3s", label="Latency", variant="warning"),
mr.Indicator(value="0.94", label="AUC", delta=0.02, variant="ml"),
])
```
## Styling
[Section titled “Styling”](#styling)
For full control, override individual color parameters. These always take precedence over `variant`.
```python
import mercury as mr
mr.Indicator(
value="$12,340",
label="Revenue",
delta=3.1,
accent_color="#7c3aed",
border_color="#ddd6fe",
value_color="#111827",
label_color="#6b7280",
)
```
`accent_color` controls the 3px top bar. `border_color`, `value_color`, and `label_color` are applied directly to the rendered HTML card.
## Indicator Params
[Section titled “Indicator Params”](#indicator-params)
### value (required)
[Section titled “value (required)”](#value-required)
**type:** `any`
Main value displayed in the card.
Special case: if `value` is a list of `Indicator` instances, they are rendered together as a row.
***
### label
[Section titled “label”](#label)
**type:** `string`
Optional label shown above the value.
Default: `""`
***
### delta
[Section titled “delta”](#delta)
**type:** `float | string | None`
Optional change badge.
* `delta > 0` → green badge with ↑
* `delta == 0` → neutral badge, no arrow
* `delta < 0` → red badge with ↓
* `string` → shown as provided, no arrow logic
* `None` → no badge
Default: `None`
***
### variant
[Section titled “variant”](#variant)
**type:** `string`
Predefined color preset. Sets `accent_color`, `border_color`, and badge colors in one go.
Semantic: `"primary"`, `"success"`, `"warning"`, `"danger"`, `"neutral"`
Domain: `"ml"`, `"info"`, `"teal"`, `"pink"`, `"orange"`
Default: `"primary"`
***
### accent\_color
[Section titled “accent\_color”](#accent_color)
**type:** `string`
Color of the 3px top accent bar. Overrides `variant` when set.
Default: `None` (resolved from `variant`)
***
### background\_color
[Section titled “background\_color”](#background_color)
**type:** `string`
Card background color.
Default: `"#ffffff"`
***
### border\_color
[Section titled “border\_color”](#border_color)
**type:** `string`
Card border color. Overrides `variant` when set.
Default: `None` (resolved from `variant`)
***
### value\_color
[Section titled “value\_color”](#value_color)
**type:** `string`
Text color of the main value.
Default: `"#111827"`
***
### label\_color
[Section titled “label\_color”](#label_color)
**type:** `string`
Text color of the label.
Default: `"#6b7280"`
***
## Notes
[Section titled “Notes”](#notes)
* `Indicator` renders HTML using the `_repr_html_` protocol.
* Multiple indicators can be grouped into a responsive row.
* Numeric deltas are formatted as percentages (`abs(delta)%`) with trailing zeros stripped. Zero is treated as a neutral state — no arrow, badge color is taken from the active `variant`.
* Explicit color params (`accent_color`, `border_color`) always take precedence over `variant`.
# JSON
> How to display and explore JSON data with the JSON widget in a Mercury App
The **JSON** widget displays JSON data in an interactive, expandable tree view. It is useful for inspecting nested structures such as API responses, configuration files, model outputs, or logs.
You can pass JSON content as:
* a Python `dict` / `list` (it will be serialized automatically), or
* a JSON `string` (it will be parsed in the frontend).
## Basic Usage
[Section titled “Basic Usage”](#basic-usage)
Display a Python dictionary:
```python
import mercury as mr
mr.JSON(
json_data={"b": [1, 2, 3], "c": ["a", "b"]},
label="Payload",
level=2
)
```
Display a JSON string:
```python
import mercury as mr
mr.JSON(
json_data='{"name": "Alice", "age": 30}',
label="User",
level=1
)
```
## Expand Level
[Section titled “Expand Level”](#expand-level)
Use `level` to control how many levels are expanded initially.
* `level=0` — fully collapsed
* `level=1` — expand the top level (default)
* `level=2` — expand two levels, etc.
```python
import mercury as mr
mr.JSON(
json_data={"a": {"b": {"c": 123}}},
level=3
)
```
Note
Very large JSON structures may become heavy to render if you expand too many levels.
## Layout
[Section titled “Layout”](#layout)
Use `position` to control where the widget is displayed:
* `"sidebar"` — in the sidebar
* `"inline"` — directly in the notebook flow (**default**)
* `"bottom"` — after all notebook cells
```python
import mercury as mr
mr.JSON(
json_data={"status": "ok"},
position="sidebar"
)
```
## JSON Props
[Section titled “JSON Props”](#json-props)
### json\_data
[Section titled “json\_data”](#json_data)
**type:** `dict | list | string | None`
JSON content to display.
* If `dict` or `list`, it is serialized to JSON automatically.
* If `string`, it is parsed as JSON in the frontend.
* If `None`, `{}` is displayed.
***
### label
[Section titled “label”](#label)
**type:** `string`
Optional label displayed above the viewer.
Default: `""`
***
### level
[Section titled “level”](#level)
**type:** `int`
Initial expand level.
Default: `1`
***
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the widget is rendered.
Default: `"inline"`
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier to distinguish widgets with identical arguments.
Note
If the same code cell is run again, Mercury reuses the previous widget instance unless a different `key` is specified.
The `key` value is needed if widgets are created in a loop.
***
## Notes
[Section titled “Notes”](#notes)
* The JSON viewer expects valid JSON types: objects (dict), arrays (list), strings, numbers, booleans, and `null`.
* Python-only types like `set` are **not valid JSON** and should be converted first (for example: `list(my_set)`).
# Markdown
> How to display Markdown content with the Markdown widget in a Mercury App
The **Markdown** widget displays Markdown-formatted text in a Mercury App. It converts Markdown to HTML and renders it as a first-class widget with full Mercury layout support.
Markdown supports headings, lists, code blocks, tables, links, and images. Mercury automatically removes scripts and custom HTML styles, including when you update the widget using `.text`.
Using custom HTML
If your existing content relies on custom HTML styles, you can pass `unsafe_allow_html=True` to `mr.Markdown()`. Use this only for content you write and trust—it also allows JavaScript. Leave it off for user input, uploaded files, or AI responses. To update Markdown content, use `.text` rather than `.value`.
The Markdown rendering dependency is **installed automatically with the `mercury` package**, so no additional setup is required.
The widget supports Mercury layout placement via the `position` argument:
* `"inline"` — render in the main notebook flow (default)
* `"sidebar"` — render in the sidebar
* `"bottom"` — render after all notebook cells
***
## Basic Usage
[Section titled “Basic Usage”](#basic-usage)
Render Markdown in the main view:
```python
import mercury as mr
_ = mr.Markdown(
"# Hello\nThis is **Markdown** rendered in Mercury."
)
```
Avoid duplicate output in notebooks
`Markdown()` displays the widget immediately and returns the widget instance. IPython also displays an unassigned object when it is the last expression in a cell, so a bare `mr.Markdown(...)` call at the end of a cell can appear twice.
Assign the result to a descriptive variable when you need the widget later, or to `_` when you only need the immediate display. Do not wrap an immediately displayed Markdown widget in an additional `display()` call.
## Layout
[Section titled “Layout”](#layout)
Use `position` to control where the Markdown is displayed:
```python
import mercury as mr
_ = mr.Markdown(
"## Sidebar note\nYou can put documentation or hints here.",
position="sidebar"
)
```
```python
import mercury as mr
_ = mr.Markdown(
"_Footer-style text_ shown at the bottom.",
position="bottom"
)
```
## Basic Markdown Syntax
[Section titled “Basic Markdown Syntax”](#basic-markdown-syntax)
The Markdown widget supports standard Markdown syntax.
### Headings
[Section titled “Headings”](#headings)
```markdown
# Heading 1
## Heading 2
### Heading 3
```
***
### Emphasis
[Section titled “Emphasis”](#emphasis)
```markdown
*italic*
**bold**
***bold and italic***
```
***
### Lists
[Section titled “Lists”](#lists)
Unordered list:
```markdown
- Item A
- Item B
- Item C
```
Ordered list:
```markdown
1. First
2. Second
3. Third
```
***
### Links
[Section titled “Links”](#links)
```markdown
[Mercury documentation](https://mljar.com/mercury)
```
***
### Code
[Section titled “Code”](#code)
Inline code:
```markdown
Use `print()` to display output.
```
***
Note
Markdown support comes from the `markdown` Python package, which is bundled with Mercury. You do not need to install anything extra.
***
## Markdown Props
[Section titled “Markdown Props”](#markdown-props)
### text
[Section titled “text”](#text)
**type:** `string`
Markdown content to render.
***
### position
[Section titled “position”](#position)
**type:** `"inline" | "sidebar" | "bottom"`
Controls where the widget is rendered.
Default: `"inline"`
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to reuse the same widget instance.
## Return value and display behavior
[Section titled “Return value and display behavior”](#return-value-and-display-behavior)
`Markdown()` returns a `MarkdownWidget` and displays it immediately. Assign the return value to prevent IPython from automatically rendering the same widget a second time when the call is the final expression in a notebook cell:
```python
message = mr.Markdown("**Rendered once**")
```
Use `_ = mr.Markdown(...)` when you do not need to access the returned widget.
# PDF
> How to display PDF documents in a Mercury App with the PDF widget
The **PDF** widget displays a PDF document inside a Mercury App using an embedded iframe. It is useful for showing reports, invoices, slides, and generated documents directly in your app.
The widget reads a local PDF file and embeds it as a `data:` URL, so the document can be rendered reliably in the Mercury UI.
## Basic Usage
[Section titled “Basic Usage”](#basic-usage)
### Display a Local PDF
[Section titled “Display a Local PDF”](#display-a-local-pdf)
```python
import mercury as mr
mr.PDF(
file_path="report.pdf",
label="Monthly report",
height="900"
)
```
## Layout
[Section titled “Layout”](#layout)
Use `position` to control where the PDF viewer is displayed:
* `"inline"` — in the notebook flow (**default**)
* `"sidebar"` — in the sidebar
* `"bottom"` — after all notebook cells
```python
import mercury as mr
mr.PDF(
file_path="slides.pdf",
position="bottom",
label="Slides"
)
```
Note
The PDF viewer is usually too wide for the sidebar. If you use `position="sidebar"`, consider reducing the width or using a smaller height.
## Sizing
[Section titled “Sizing”](#sizing)
### Width
[Section titled “Width”](#width)
```python
import mercury as mr
mr.PDF(
file_path="report.pdf",
width="100%"
)
```
Default: `"100%"`
### Height
[Section titled “Height”](#height)
```python
import mercury as mr
mr.PDF(
file_path="report.pdf",
height="600"
)
```
Default: `"800"`
## Example: Generate PDF and Display It
[Section titled “Example: Generate PDF and Display It”](#example-generate-pdf-and-display-it)
A common workflow is to generate a PDF first (for example with ReportLab), save it to disk, then display it with `mr.PDF()`.
```python
import mercury as mr
from reportlab.pdfgen import canvas
pdf_path = "hello.pdf"
c = canvas.Canvas(pdf_path)
c.drawString(72, 720, "Hello from Mercury ✅")
c.save()
mr.PDF(
file_path=pdf_path,
label="Generated PDF"
)
```
## PDF Props
[Section titled “PDF Props”](#pdf-props)
### file\_path
[Section titled “file\_path”](#file_path)
**type:** `string | None`
Path to a local PDF file.
If `None`, the widget is created without a document (empty iframe).
Default: `None`
***
### label
[Section titled “label”](#label)
**type:** `string`
Optional label displayed above the viewer.
Default: `""`
***
### width
[Section titled “width”](#width-1)
**type:** `string`
CSS width of the iframe (for example `"100%"`, `"800px"`).
Default: `"100%"`
***
### height
[Section titled “height”](#height-1)
**type:** `string`
Height of the iframe. Usually a pixel value like `"800"` or `"900"`.
Default: `"800"`
***
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the widget is rendered.
Default: `"inline"`
***
### key
[Section titled “key”](#key)
**type:** `string`
Unique identifier used to distinguish widgets with identical arguments.
***
## Notes
[Section titled “Notes”](#notes)
* The PDF is embedded using an iframe with a `data:application/pdf;base64,...` URL.
* Very large PDFs may increase memory usage because they are encoded into base64.
* The widget is best used for local PDF files. If you want to show remote PDFs, download them locally first.
# ProgressBar
> Display and control progress indicators in a Mercury App
The **ProgressBar** widget displays a horizontal progress indicator that can be updated dynamically from Python.\
It supports both **determinate** (percentage-based) and **indeterminate** (animated) modes and integrates with Mercury layout placement.
This widget is ideal for:
* long-running computations
* data processing pipelines
* model training or evaluation
* file uploads / downloads
* multi-step workflows
## Basic Usage
[Section titled “Basic Usage”](#basic-usage)
### Determinate Progress
[Section titled “Determinate Progress”](#determinate-progress)
```python
import mercury as mr
progress = mr.ProgressBar(
label="Processing data",
value=0
)
progress.set(25)
progress.set(50)
progress.set(75)
progress.set(100)
```
The progress bar updates immediately when `set()` is called.
## Indeterminate Mode
[Section titled “Indeterminate Mode”](#indeterminate-mode)
Use indeterminate mode when progress cannot be measured.
```python
import mercury as mr
import time
progress = mr.ProgressBar(
label="Loading",
indeterminate=True
)
time.sleep(2)
progress.set_indeterminate(False)
progress.set(100)
```
Note
Indeterminate mode shows an animated bar and hides the percentage value.
## Updating the Label
[Section titled “Updating the Label”](#updating-the-label)
You can update the label text dynamically:
```python
import mercury as mr
progress = mr.ProgressBar(label="Step 1/3")
progress.set_label("Step 2/3")
progress.set_label("Step 3/3")
```
## Layout
[Section titled “Layout”](#layout)
Control where the progress bar is rendered using `position`:
```python
import mercury as mr
mr.ProgressBar(
label="Sidebar task",
position="sidebar"
)
```
Available values:
* `"inline"` — main view (default)
* `"sidebar"` — left sidebar
* `"bottom"` — bottom area
## ProgressBar Params
[Section titled “ProgressBar Params”](#progressbar-params)
### label
[Section titled “label”](#label)
**type:** `string`
Optional text displayed above the progress bar.
Default: `""`
***
### value
[Section titled “value”](#value)
**type:** `float`
Initial progress value for determinate mode.
Default: `0`
***
### min / max
[Section titled “min / max”](#min--max)
**type:** `float`
Value range used to compute percentage.
Defaults:
* `min = 0`
* `max = 100`
***
### show\_percent
[Section titled “show\_percent”](#show_percent)
**type:** `bool`
Show or hide the percentage text.
Default: `True`
***
### indeterminate
[Section titled “indeterminate”](#indeterminate)
**type:** `bool`
Start the progress bar in animated indeterminate mode.
Default: `False`
***
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls widget placement in the Mercury layout.
Default: `"inline"`
***
### key
[Section titled “key”](#key)
**type:** `string`
Stable identifier used to reuse the same widget instance.
***
## ProgressHandle API
[Section titled “ProgressHandle API”](#progresshandle-api)
The `ProgressBar()` function returns a **ProgressHandle** object.
### Common Methods
[Section titled “Common Methods”](#common-methods)
* `set(value)` — update progress value
* `set_label(text)` — update label text
* `set_indeterminate(on=True)` — toggle indeterminate mode
* `show()` — display the progress bar
* `hide()` — hide the progress bar
***
## Notes
[Section titled “Notes”](#notes)
* Progress is clamped to the `[min, max]` range.
* Setting a value disables indeterminate mode automatically.
* The widget is rendered immediately when created.
# Sankey
> Display source-target-value flows as a responsive Sankey diagram
The `Sankey` output widget displays how a numeric quantity moves between stages or categories. Each rectangle is a node, and each ribbon’s width represents its value. Flows are arranged from left to right.
Prepare the flows before constructing the widget. `Sankey` aggregates duplicate source-target pairs, but it does not derive flows from raw event data.
## Basic usage
[Section titled “Basic usage”](#basic-usage)
```python
import pandas as pd
import mercury as mr
df = pd.DataFrame({
"source": ["Visitors", "Visitors", "Signup", "Signup"],
"target": ["Signup", "Left", "Paid", "Free"],
"value": [800, 200, 120, 680],
})
mr.Sankey(
df,
source="source",
target="target",
value="value",
)
```
You can also pass three-item tuples without creating a DataFrame:
```python
mr.Sankey([
("Visitors", "Signup", 800),
("Visitors", "Left", 200),
("Signup", "Paid", 120),
("Signup", "Free", 680),
])
```
## Customize the diagram
[Section titled “Customize the diagram”](#customize-the-diagram)
```python
customer_flows = df.rename(columns={"value": "customers"})
mr.Sankey(
customer_flows,
source="source",
target="target",
value="customers",
height=500,
show_values=True,
value_format=",",
colors=[
"#3b82f6",
"#22c55e",
"#f59e0b",
"#ef4444",
],
)
```
`colors` accepts a list that cycles through nodes or a dictionary that assigns colors to specific node names:
```python
mr.Sankey(
df,
colors={
"Visitors": "#2563eb",
"Signup": "#16a34a",
"Paid": "#f59e0b",
},
)
```
Links use their source node’s color. `link_opacity` controls ribbon transparency. Mercury theme colors are used when `colors` is omitted or a dictionary does not map every node.
## Input requirements
[Section titled “Input requirements”](#input-requirements)
Every row must contain a non-empty source, a non-empty target, and a finite, non-negative numeric value. Zero-value links are omitted, and duplicate links are summed.
Sankey diagrams currently require an acyclic graph. Self-links and cycles raise clear errors rather than producing an ambiguous layout.
```text
source | target | value
```
## Labels, values, and tooltips
[Section titled “Labels, values, and tooltips”](#labels-values-and-tooltips)
Node names are always visible. Enable `show_values` to append each node’s total flow value to its label. The value is the larger of its total incoming and outgoing flow.
`value_format` accepts a standard Python numeric format specification:
```python
mr.Sankey(df, show_values=True, value_format=",")
```
Nodes and ribbons include native browser tooltips. The SVG also includes accessible labels and a description, so values are not communicated through color alone.
## Responsive layout
[Section titled “Responsive layout”](#responsive-layout)
The SVG fills the available width. Diagrams with many stages keep a readable minimum width and scroll horizontally inside narrow layouts such as `mr.Columns(2)`.
## Parameters
[Section titled “Parameters”](#parameters)
| Parameter | Description | Default |
| -------------- | ------------------------------------------------ | ---------- |
| `data` | DataFrame, three-item tuples, or dictionaries | required |
| `source` | Source column or dictionary key | `"source"` |
| `target` | Target column or dictionary key | `"target"` |
| `value` | Numeric value column or dictionary key | `"value"` |
| `colors` | Hex color list, node-color dictionary, or `None` | `None` |
| `height` | SVG layout height | `400` |
| `node_width` | Node rectangle width | `16` |
| `node_padding` | Vertical spacing between nodes | `16` |
| `link_opacity` | Ribbon opacity between zero and one | `0.35` |
| `show_values` | Append values to node labels | `False` |
| `value_format` | Python numeric format specification | `None` |
The implementation is generated entirely in Python and rendered as SVG. It does not load Plotly, D3, or another JavaScript charting library.
# SplitFlap
> Display animated numbers and short text on a retro split-flap board
The **SplitFlap** output widget presents numbers and short text like an old airport or railway departure board. Changed characters rotate through a two-part mechanical flip, making it useful for KPIs, counters, rankings, scores, and dashboard headlines.
## Basic usage
[Section titled “Basic usage”](#basic-usage)
```python
import mercury as mr
board = mr.SplitFlap(value="12,482")
```
`SplitFlap()` displays the board and returns the live widget object.
## Sizes
[Section titled “Sizes”](#sizes)
Use `size="small"`, `"medium"`, or `"large"`:
```python
mr.SplitFlap(
value="$42,810",
size="large",
)
```
The default is `"medium"`. Boards keep their characters on one line and scroll horizontally in a container that is too narrow.
## Live updates
[Section titled “Live updates”](#live-updates)
Call `set()` to change the value of an existing board:
```python
scoreboard = mr.SplitFlap("042")
scoreboard.set("105")
```
In a served Mercury app, use a stable `key` and drive changes with an input widget in an earlier cell. Each `# %%` below represents a separate notebook cell:
```python
# %%
import mercury as mr
# %%
score = mr.Slider(
label="Score",
value=42,
min=0,
max=999,
position="inline",
)
# %%
scoreboard = mr.SplitFlap(
value=f"{score.value:03}",
size="large",
key="scoreboard",
)
```
Moving the slider reruns the cell below it. The stable key reuses the same frontend board, so changed digits visibly flip rather than creating a second widget.
## Animation
[Section titled “Animation”](#animation)
Animation is enabled by default and applies only to character positions that change. Adjacent changes have a short mechanical stagger. Disable it when an immediate update is more appropriate:
```python
mr.SplitFlap("ON TIME", animate=False)
```
Motion is also removed automatically when the browser requests reduced motion. The new value remains available as an accessible live status announcement.
## Values
[Section titled “Values”](#values)
`value` accepts a string, integer, or float. Numbers are converted to text without automatic currency or thousands formatting, so supply the exact presentation you want:
```python
mr.SplitFlap(f"${revenue:,.0f}")
```
Capitalization, punctuation, and spaces are preserved. Length changes are supported.
Use newline characters to create a single multi-row board. Pad the strings when you want table-like column alignment:
```python
mr.SplitFlap(
"SYMBOL PRICE CHANGE\n"
"AAPL $231.42 ▲0.10%\n"
"MSFT $417.10 ▼0.10%",
size="small",
)
```
Rows share one board frame and changed characters animate independently.
## Theme configuration
[Section titled “Theme configuration”](#theme-configuration)
`SplitFlap` renders the board directly, without a surrounding card, border, or label. Its accent and corner radius follow Mercury’s shared theme variables, which can be changed in `config.toml`:
```toml
[theme]
primary_color = "#ffb000"
border_radius = "6px"
```
The character tiles retain their dark, high-contrast mechanical appearance.
## Parameters
[Section titled “Parameters”](#parameters)
### `value`
[Section titled “value”](#value)
Required string, integer, or float displayed on the board.
### `size`
[Section titled “size”](#size)
`"small"`, `"medium"`, or `"large"`. Default: `"medium"`.
### `animate`
[Section titled “animate”](#animate)
Animate character changes. Default: `True`.
### `position`
[Section titled “position”](#position)
Mercury layout placement: `"inline"`, `"sidebar"`, or `"bottom"`. Default: `"inline"`.
### `key`
[Section titled “key”](#key)
Stable identifier used to reuse and update the widget across reactive cell executions.
# StatusLights
> Display animated service and workflow status lamps in a Mercury App
The **StatusLights** output widget displays a group of compact status lamps inspired by industrial control panels and classic computer consoles. It is useful for service monitoring, pipelines, ETL jobs, ML workflows, and agent execution.
## Basic usage
[Section titled “Basic usage”](#basic-usage)
```python
import mercury as mr
lights = mr.StatusLights({
"Database": "ok",
"API": "ok",
"Model": "warning",
"Worker": "error",
})
```
Lamps are rendered in dictionary insertion order and automatically wrap into a responsive grid.
Set `orientation="vertical"` to display a single-column stack instead:
```python
mr.StatusLights(
{"Database": "ok", "API": "active", "Worker": "off"},
orientation="vertical",
)
```
## States
[Section titled “States”](#states)
Five states are available:
| State | Appearance |
| ----------- | ------------ |
| `"off"` | Dim gray |
| `"ok"` | Green |
| `"warning"` | Amber |
| `"error"` | Red |
| `"active"` | Mercury blue |
State names are case-insensitive and normalized to lowercase.
## Animation
[Section titled “Animation”](#animation)
Pass lamp labels to `blink` or `pulse`:
```python
lights = mr.StatusLights(
{
"Database": "ok",
"Model": "warning",
"Worker": "error",
},
blink=["Worker"],
pulse=["Model"],
)
```
A lamp cannot blink and pulse simultaneously. Animation is disabled automatically when the browser requests reduced motion; the color and textual state remain visible.
## Live updates
[Section titled “Live updates”](#live-updates)
`StatusLights()` returns the displayed widget. Update it directly while code is running:
```python
lights = mr.StatusLights(
{"Extract": "active", "Transform": "off", "Load": "off"},
pulse=["Extract"],
key="etl",
)
lights.set("Extract", "ok")
lights.set("Transform", "active")
lights.set_animation("Extract", None)
lights.set_animation("Transform", "pulse")
lights.update({
"Transform": "ok",
"Load": "active",
})
```
Available update methods:
* `set(name, state)` adds or updates one lamp in place.
* `update(statuses)` merges several lamp states in place.
* `set_animation(name, "blink")` enables blinking.
* `set_animation(name, "pulse")` enables pulsing.
* `set_animation(name, None)` removes animation.
## Title and placement
[Section titled “Title and placement”](#title-and-placement)
```python
mr.StatusLights(
{"API": "ok", "Queue": "active"},
title="Production",
position="sidebar",
key="production-health",
)
```
`position` accepts `"inline"`, `"sidebar"`, or `"bottom"`.
The title and both border levels can be hidden independently:
```python
mr.StatusLights(
{"API": "ok", "Queue": "active"},
title="Production",
show_title=False,
show_group_border=False,
show_item_borders=False,
)
```
Leaving `title=""` also omits the title automatically.
## Theme configuration
[Section titled “Theme configuration”](#theme-configuration)
StatusLights uses Mercury’s shared CSS variables for typography, surfaces, borders, radii, shadows, and semantic colors. Changes to `config.toml` therefore apply to the lamps and their containers:
```toml
[theme]
primary_color = "#00d9ff" # active
success_color = "#00ff85" # ok
warning_color = "#ffe600" # warning
danger_color = "#ff1744" # error
```
The radial highlight, saturation, and multi-layer glow are applied on top of those configured colors to produce the neon lamp effect.
Use a stable `key` when statuses are recreated by a reactive notebook cell or when multiple status groups appear in the same cell. The existing widget is reused and updated instead of creating another frontend model.
## Parameters
[Section titled “Parameters”](#parameters)
### `statuses`
[Section titled “statuses”](#statuses)
Required mapping of non-empty labels to supported state names. An empty mapping is allowed and can be populated later with `set()` or `update()`.
### `blink` / `pulse`
[Section titled “blink / pulse”](#blink--pulse)
Optional sequences of labels from `statuses`.
### `title`
[Section titled “title”](#title)
Optional heading displayed above the lamps. Default: `""`.
### `orientation`
[Section titled “orientation”](#orientation)
`"horizontal"` for a responsive row or `"vertical"` for a single-column stack. Default: `"horizontal"`.
### `show_title`
[Section titled “show\_title”](#show_title)
Show a non-empty title. Default: `True`.
### `show_group_border`
[Section titled “show\_group\_border”](#show_group_border)
Show the outer panel surface and border. Default: `True`.
### `show_item_borders`
[Section titled “show\_item\_borders”](#show_item_borders)
Show the surface and border around each lamp item. Default: `True`.
### `position`
[Section titled “position”](#position)
Mercury layout placement. Default: `"inline"`.
### `key`
[Section titled “key”](#key)
Stable identifier used for widget reuse across cell executions.
# Table
> How to use the Table widget in the interactive Mercury App
The **Table** widget displays data in a structured, interactive tabular format. It supports column sorting (ascending/descending), quick filtering, and row selection for easier data exploration.
## Import
[Section titled “Import”](#import)
```python
from mercury import Table
```
## Usage
[Section titled “Usage”](#usage)
To use the Table widget, you only need to provide data. It works best with DataFrames (pandas or polars), but you can also pass lists of dict or dicts.
**Example data**
```python
import pandas as pd
countries = ["Poland", "Germany", "France"]
df = pd.DataFrame({
"Country": [countries[i % 3] for i in range(100)],
"Year": [2020 + (i % 10) for i in range(100)],
"GDP": [500 + (i * 25) for i in range(100)],
})
```
**Code**
```python
t = Table(df)
```
Avoid duplicate output in notebooks
With the default `display_now=True`, `Table()` displays the table immediately and returns its widget. IPython also displays an unassigned object when it is the last expression in a cell, so a bare `Table(df)` call can render twice.
Assign the result, as in `table = Table(df)`, when using immediate display. If you want to control when it appears, use `display_now=False` and call `display(table)` later—but do not combine explicit `display()` with the default immediate-display behavior.
**Preview**

### Table with features
[Section titled “Table with features”](#table-with-features)
You can easily add more features to your table, such as row selection or filtering.
**Code**
```python
t = Table(df, search=True, select_rows=True)
```
**Preview**

### Get selected rows
[Section titled “Get selected rows”](#get-selected-rows)
After enabling row selection, you may want to access all selected rows in one place — this is very easy to do.
**Code**
```python
# t is the variable that you assign the Table to
t.selected_rows
```
### Customize your Table
[Section titled “Customize your Table”](#customize-your-table)
You can also customize the Table’s appearance by changing the page size (number of rows per page), width, or height.
**Code**
```python
t = Table(
df,
search=True,
select_rows=True,
page_size=20,
width="700px",
height="360px",
)
```
**Preview**

### Create without immediate display
[Section titled “Create without immediate display”](#create-without-immediate-display)
By default, `Table()` displays the widget immediately. Set `display_now=False` when you want to create the widget first and display it later.
**Code**
```python
from IPython.display import display
t = Table(df, display_now=False)
# display later
display(t)
```
## Table Props
[Section titled “Table Props”](#table-props)
### data (required)
[Section titled “data (required)”](#data-required)
**type:** `DataFrame` or `list[dict]` or `dict`
The dataset displayed in the table. Accepts pandas/polars DataFrames or standard Python structures like lists of dictionaries.
### page\_size
[Section titled “page\_size”](#page_size)
**type:** `Integer`
Sets how many rows are shown on a single page. Default is `50`.
### select\_rows
[Section titled “select\_rows”](#select_rows)
**type:** `boolean`
Enables selecting rows in the table. It’s off by default (`False`).
### search
[Section titled “search”](#search)
**type:** `boolean`
Enables the search bar and allows filtering table. It’s off by default (`False`).
### width
[Section titled “width”](#width)
**type:** `string`
Sets the width of the table component. Accepts CSS size values like `100%`, `800px`, `50vw`, or `48rem`. Default is `100%`.
### height
[Section titled “height”](#height)
**type:** `string | None`
Sets the height of the scrollable table body. Accepts CSS size values like `360px`, `50vh`, or `24rem`. When `None`, Mercury keeps the current automatic table height behavior. Default is `None`.
### show\_index\_col
[Section titled “show\_index\_col”](#show_index_col)
**type:** `boolean`
Controls whether the original DataFrame index is displayed as a table column. Default is `False`.
### position
[Section titled “position”](#position)
**type:** `"sidebar" | "inline" | "bottom"`
Controls where the table is rendered in the Mercury layout. Default is `"inline"`.
### display\_now
[Section titled “display\_now”](#display_now)
**type:** `boolean`
Controls whether the table is displayed immediately after construction. Set to `False` when you want to return the widget and display it later yourself. Default is `True`.
When `display_now=True`, assign the returned widget to a variable (or `_`) so IPython does not display the final expression a second time.
### key
[Section titled “key”](#key)
**type:** `string`
Optional identifier used to distinguish tables with the same data and options. Use it when you intentionally want separate table widget instances for the same dataset.
## Notes
[Section titled “Notes”](#notes)
* Table widgets are cached based on the input data content and constructor options.
* `display_now` is not part of the cache key. It only controls whether the widget is displayed immediately.
* Use `key` to force separate table instances for the same data.
* Use either immediate display with assignment or `display_now=False` followed by `display()`, not both.
# Teletype
> Reveal plain text like an old terminal and append live output in a Mercury App
The **Teletype** output widget reveals plain text character by character like an old terminal or teleprinter. It is useful for agent execution, AI output, logs, simulations, data-processing status, and narrative dashboards.
## Basic usage
[Section titled “Basic usage”](#basic-usage)
```python
import mercury as mr
output = mr.Teletype(
"Loading dataset...\n"
"Analyzing 18,420 rows...\n"
"Done."
)
```
Spaces, tabs, and newline characters are preserved. `Teletype` deliberately renders plain text rather than Markdown or HTML, so partially revealed content is always safe and visually stable.
## Speed and cursor
[Section titled “Speed and cursor”](#speed-and-cursor)
`speed` is the delay in milliseconds per visible character:
```python
status = mr.Teletype(
"SYSTEM READY",
speed=30,
cursor=True,
)
```
The default is `30`. Set `speed=0` to display content immediately, which is useful when an AI provider or another process already controls the arrival rate. The cursor remains solid while text is being written and blinks while idle. Hide it with `cursor=False`.
Animation and cursor blinking are removed automatically when the browser requests reduced motion. Emoji and combined Unicode characters are revealed as complete visible characters rather than being split into invalid fragments.
## Append text incrementally
[Section titled “Append text incrementally”](#append-text-incrementally)
`Teletype()` returns its live widget object. Call `append()` from later notebook code to add output without replaying text that is already present:
```python
# %%
import mercury as mr
# %%
terminal = mr.Teletype("", speed=20, key="agent-log")
# %%
terminal.append("Loading dataset...")
terminal.append("\nAnalyzing 18,420 rows...")
terminal.append("\nDone.")
```
Each `# %%` above starts a separate notebook cell. If new text arrives while the widget is still typing, it joins the same animation queue instead of interrupting or restarting it.
For a real streaming response, append each plain-text chunk as it arrives:
```python
# %%
response = mr.Teletype("", speed=0, key="model-response")
# %%
for chunk in model.generate_stream(prompt):
response.append(chunk)
```
Use Mercury’s chat messages instead when the output needs Markdown formatting or chat roles.
## Replace or clear the output
[Section titled “Replace or clear the output”](#replace-or-clear-the-output)
Assign to `text` or call `set()` to replace the target text:
```python
terminal.text = "Starting another operation..."
terminal.set("Operation complete.")
terminal.clear()
```
Unrelated replacement text starts a fresh reveal. If the assigned text is an exact extension of the current target, only its new suffix is queued. Assigning identical text does nothing, preventing reactive notebook reruns from replaying the animation.
## Reactive updates
[Section titled “Reactive updates”](#reactive-updates)
Use a stable `key` when a lower notebook cell recreates the output:
```python
# %%
import mercury as mr
# %%
stage = mr.Slider(label="Stage", value=1, min=1, max=3)
# %%
messages = [
"Connecting...",
"Connecting...\nDownloading records...",
"Connecting...\nDownloading records...\nDone.",
]
terminal = mr.Teletype(
messages[stage.value - 1],
speed=24,
key="pipeline-status",
)
```
Moving the slider reruns the cell below it. The stable key reuses the frontend widget, so only the newly added suffix is animated.
## Auto-scroll
[Section titled “Auto-scroll”](#auto-scroll)
With the default `auto_scroll=True`, new output remains visible while the reader is already near the bottom of its scrolling container. If the reader scrolls upward to inspect previous output, the widget stops pulling the view downward. Set `auto_scroll=False` to disable this behavior completely.
## Theme configuration
[Section titled “Theme configuration”](#theme-configuration)
The surface, text, border, radius, font size, and cursor colors use Mercury’s runtime theme variables generated from `config.toml`. For example:
```toml
[theme]
card_background_color = "#101419"
text_color = "#d8f3dc"
border_color = "#324238"
primary_color = "#52d273"
accent_color = "#7bf1a8"
border_radius = "6px"
font_size = "15px"
```
The output uses a system monospace font stack while inheriting the surrounding application’s configured colors.
## Parameters
[Section titled “Parameters”](#parameters)
### `text`
[Section titled “text”](#text)
Required plain-text string. Whitespace and line breaks are preserved.
### `speed`
[Section titled “speed”](#speed)
Finite non-negative milliseconds per visible character. Default: `30`. Use `0` for immediate display.
### `cursor`
[Section titled “cursor”](#cursor)
Show the terminal cursor. Default: `True`.
### `auto_scroll`
[Section titled “auto\_scroll”](#auto_scroll)
Keep new output visible while the reader remains near the bottom. Default: `True`.
### `position`
[Section titled “position”](#position)
Mercury layout placement: `"inline"`, `"sidebar"`, or `"bottom"`. Default: `"inline"`.
### `key`
[Section titled “key”](#key)
Stable identifier used to reuse the widget across reactive cell executions.
## Accessibility
[Section titled “Accessibility”](#accessibility)
Visual text is hidden from assistive technology while a separate polite log announces complete appended chunks. Screen readers therefore do not announce every individual character. The visual cursor is not included in selected or copied text.
# VUMeter
> Display live values on a retro analog instrument in a Mercury App
The **VUMeter** output widget presents a numeric value like a physical analog instrument. Its needle moves smoothly when the value changes, making it useful for utilization, confidence, performance, anomaly, and percentage metrics.
## Basic usage
[Section titled “Basic usage”](#basic-usage)
```python
import mercury as mr
meter = mr.VUMeter(
value=73,
min=0,
max=100,
label="CPU LOAD",
)
```
`VUMeter()` displays the instrument and returns its live widget object.
The dial uses one thin Indicator-style container border with no additional casing or inner outline.
## Zones
[Section titled “Zones”](#zones)
Pass two thresholds to divide the scale into three semantic zones:
```python
mr.VUMeter(
value=0.82,
min=0,
max=1,
label="MODEL CONFIDENCE",
zones=[0.5, 0.8],
)
```
By default, higher values are considered better. The zones progress from danger to warning to success. For metrics where increasing values indicate trouble, reverse the colors with `higher_is_better=False`:
```python
mr.VUMeter(
value=73,
min=0,
max=100,
label="CPU LOAD",
zones=[60, 85],
higher_is_better=False,
)
```
Without `zones`, the scale uses Mercury’s primary color.
## Reactive updates
[Section titled “Reactive updates”](#reactive-updates)
Use a stable `key` when a reactive notebook cell recreates the meter. Each `# %%` below marks a separate notebook cell:
```python
# %%
import mercury as mr
# %%
load = mr.Slider(
label="CPU load",
value=42,
min=0,
max=100,
)
# %%
meter = mr.VUMeter(
value=load.value,
min=0,
max=100,
label="CPU LOAD",
zones=[60, 85],
higher_is_better=False,
key="cpu-meter",
)
```
Changing the slider reruns the meter cell. Its stable key preserves the frontend instrument, allowing the needle to animate from its previous position.
You can also update a displayed meter directly from a later cell:
```python
meter.value = 73
meter.set(88)
```
The exact value is deliberately not repeated below the needle. If you want it visible, include it in the face label:
```python
cpu = 73
mr.VUMeter(
value=cpu,
label=f"CPU LOAD · {cpu}%",
)
```
In a reactive app, build the label from the same input value used by the meter.
Out-of-range values are clamped to the physical scale and emit a Python warning.
## Animation and sizes
[Section titled “Animation and sizes”](#animation-and-sizes)
The initial value is positioned immediately. Later changes use a lightweight CSS transform that retargets smoothly during rapid updates. Disable it with `animate=False`:
```python
mr.VUMeter(42, label="SIGNAL", animate=False)
```
Use `size="small"`, `"medium"`, or `"large"`. The instrument remains responsive and shrinks when its container is narrower than the selected maximum width.
## Theme configuration
[Section titled “Theme configuration”](#theme-configuration)
The dial, typography, needle, zones, and container use runtime Mercury CSS variables generated from `config.toml`. The meter background uses `card_background_color`, with `panel_bg` as its fallback, matching its role as a dashboard card. For example:
```toml
[theme]
font_family = "IBM Plex Mono, ui-monospace, monospace"
card_background_color = "#f4e9c8"
panel_bg = "#f4e9c8"
text_color = "#231f1a"
muted_text_color = "#625b4e"
border_color = "#776b58"
primary_color = "#d97706"
success_color = "#31965a"
warning_color = "#d99b21"
danger_color = "#c43d32"
border_radius_lg = "12px"
shadow_md = "0 8px 22px rgba(0, 0, 0, 0.24)"
```
The physical highlights and shading are mixed from these configured colors, so the same meter adapts to light, dark, and branded applications.
## Parameters
[Section titled “Parameters”](#parameters)
### `value`
[Section titled “value”](#value)
Required finite numeric value. Values outside the scale are clamped.
### `min` / `max`
[Section titled “min / max”](#min--max)
Finite scale bounds where `min < max`. Defaults: `0` and `100`.
### `label`
[Section titled “label”](#label)
Text printed inside the instrument face. Default: `""`, displayed as `"VU"`.
### `zones`
[Section titled “zones”](#zones-1)
Optional sequence of exactly two ascending thresholds strictly inside the scale.
### `higher_is_better`
[Section titled “higher\_is\_better”](#higher_is_better)
If `True`, zones progress from danger to success. If `False`, they progress from success to danger. Default: `True`.
### `size`
[Section titled “size”](#size)
`"small"`, `"medium"`, or `"large"`. Default: `"medium"`.
### `animate`
[Section titled “animate”](#animate)
Animate needle changes. Default: `True`. Browser reduced-motion preferences are respected automatically.
### `position`
[Section titled “position”](#position)
Mercury layout placement: `"inline"`, `"sidebar"`, or `"bottom"`. Default: `"inline"`.
### `key`
[Section titled “key”](#key)
Stable identifier used to reuse the meter across reactive cell executions.
## Accessibility
[Section titled “Accessibility”](#accessibility)
The instrument uses the semantic `meter` role with its current value and bounds. The exact value is available to assistive technology, and the scale remains readable without relying on animation or color alone.
# Quick Start
> Quick start into Mercury framework
By default, Mercury detects all Python notebooks available in the current directory and serves them as web apps. Please run the following command to start the server:
Start Mercury Server
```bash
mercury
```
It should open a web page at `localhost:8888` with information that no notebooks were found. If there are no `*.ipynb` files in the current directory, you should see:

## Create a notebook
[Section titled “Create a notebook”](#create-a-notebook)
Let’s create a first notebook with `TextInput` widget. The notebook will ask for user name and display welcome message.
```python
# first cell with imports
from mercury import TextInput
```
```python
# create interactive widget
name = TextInput(label="What is yout name?")
```
```python
# display widget value
print(f"Hey {name.value}!")
```
## Live preview
[Section titled “Live preview”](#live-preview)
In **MLJAR Studio** or **JupyterLab** editor please click on confetti icon in the top toolbar to open a live app preview.

Cell re-execution in live app preview
Mercury automatically re-execute cells below updated widget. This feature is available only when serving notebooks with Mercury or when live preview is open during development.
## Start server
[Section titled “Start server”](#start-server)
Start a server with command:
```bash
mercury
```
If you want Mercury to use a different base directory for notebooks, `config.toml`, and relative file access, start it like this:
```bash
mercury --working-dir /path/to/notebooks
```
You should see a card with notebook. The title in the card will be the same as notebook filename.
 
# Examples
> Example Mercury web apps
Find your inspiration
## Chat bots code examples
[Section titled “Chat bots code examples”](#chat-bots-code-examples)
[Echo chat web app in Python ](/examples/chat/echo-chat-web-app-python/)Build basic echo chat web app in Python.
[Matplotlib charts in Message ](/examples/chat/chat-message-with-matplotlib-chart/)Use Matplotlib to create visualizations in your chat.
[Altair charts in Message ](/examples/chat/chat-message-with-altair-chart/)Use Altair to create visualizations in your chat.
# Chat message with an Altair chart
> Learn how to display interactive Altair charts inside chat messages. In this example, we use a Python notebook and the Mercury framework to build a chat bot that responds with Altair visualizations.
Mercury chat [`Message`](/docs/chat/message/) can display **interactive charts**, tables, and rich outputs. In this example, the assistant responds with an **Altair chart** based on sales data.
Note
This bot respons to every message with the same chart.
## App preview
[Section titled “App preview”](#app-preview)

Try the live web app:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/chat-altair-charts.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/chat-altair-charts?no-navbar)
## 1. Import Mercury and Altair
[Section titled “1. Import Mercury and Altair”](#1-import-mercury-and-altair)
In the first notebook cell, import required libraries:
```python
import mercury as mr
import pandas as pd
import altair as alt
```
We use following aliases:
* `mr` for Mercury widgets
* `pandas` for data
* `altair` for interactive charts
## 2. Example data
[Section titled “2. Example data”](#2-example-data)
Prepare a simple sales dataset:
```python
sales_df = pd.DataFrame({
"date": ["2023-01-01", "2023-01-08", "2023-01-15", "2023-01-22"],
"region": ["North", "South", "East", "West"],
"product": ["A", "B", "A", "C"],
"units_sold": [10, 15, 7, 12],
"unit_price": [20, 25, 19, 22],
})
sales_df["total_sales"] = sales_df["units_sold"] * sales_df["unit_price"]
```
## 3. Create chat and input
[Section titled “3. Create chat and input”](#3-create-chat-and-input)
Create the [`Chat`](/docs/chat/chat/) widget:
```python
chat = mr.Chat()
```
In the next cell, create the [`ChatInput`](/docs/chat/chatinput/) widget:
```python
prompt = mr.ChatInput()
```
Note
`Chat` and `ChatInput` should be created in separate cells.\
Mercury places widgets from the same cell in the same position, and `ChatInput` is placed at the bottom by default.
## 4. Respond with an Altair chart
[Section titled “4. Respond with an Altair chart”](#4-respond-with-an-altair-chart)
When the user sends a message:
1. The cell below `ChatInput` is re-executed
2. The user message is added to the chat
3. The assistant creates an Altair chart
4. The chart is displayed **inside the chat message**
```python
if prompt.value:
# user message
user_msg = mr.Message(prompt.value, role="user")
chat.add(user_msg)
# assistant message
response_msg = mr.Message(
"**Altair** plot based on sales data",
role="assistant",
emoji="🤖",
)
with response_msg:
# prepare data
df = sales_df.copy()
df["date"] = pd.to_datetime(df["date"])
# create Altair chart
chart = (
alt.Chart(df)
.mark_line(point=True)
.encode(
x=alt.X("date:T", title="Date"),
y=alt.Y("total_sales:Q", title="Total Sales"),
tooltip=["date:T", "total_sales:Q"],
)
.properties(
width=400,
height=250,
title="Total Sales Over Time",
)
)
# display chart in the message
display(chart)
chat.add(response_msg)
```
## Why this works
[Section titled “Why this works”](#why-this-works)
* `ChatInput` automatically re-runs the cell below it
* `prompt.value` contains the latest user message
* Your code reacts and appends new messages to the chat
No callbacks. No JavaScript. No async code.
# Chat message with a Matplotlib chart
> In this example, you will learn how to include charts in your chat web apps. We will use Python notebook and Mercury framework to create chat web app. The chat bot can respond with charts from Matplotlib.
Mercury chat [`Message`](/docs/chat/message/) can display **charts, tables, and rich outputs**, not only text. In this example, the assistant responds with a **Matplotlib chart** based on sales data.
Note
This bot respons to every message with the same chart.
## App preview
[Section titled “App preview”](#app-preview)

You can try the live web app:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/chat-matplotlib-charts.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/chat-matplotlib-charts?no-navbar)
## 1. Import Mercury
[Section titled “1. Import Mercury”](#1-import-mercury)
In the first notebook cell, import Mercury:
```python
import mercury as mr
```
We will use `mr` alias to create all widgets.
## 2. Example data
[Section titled “2. Example data”](#2-example-data)
In the second cell, let’s prepare a simple sales dataset.
```python
import pandas as pd
sales_df = pd.DataFrame({
"date": ["2023-01-01", "2023-01-08", "2023-01-15", "2023-01-22"],
"region": ["North", "South", "East", "West"],
"product": ["A", "B", "A", "C"],
"units_sold": [10, 15, 7, 12],
"unit_price": [20, 25, 19, 22],
})
sales_df["total_sales"] = sales_df["units_sold"] * sales_df["unit_price"]
```
## 3. Create chat and input
[Section titled “3. Create chat and input”](#3-create-chat-and-input)
Now create the [`Chat`](/docs/chat/chat/) area, it will display all messages.
```python
chat = mr.Chat()
```
In the next cell, please create the [`ChatInput`](/docs/chat/chatinput/) to accept user prompts:
```plaintext
prompt = mr.ChatInput()
```
Note
Please note that `mr.Chat()` and `mr.ChatInput()` can’t be in the same cell, because otherwise both will be placed at the bottom of application. The Mercury framework place all widgets from the same cell in the same position. The default `position` for `ChatInput` is `bottom`.
## 4. Respond with a chart
[Section titled “4. Respond with a chart”](#4-respond-with-a-chart)
When the user sends a message:
1. We add the user message to the chat
2. We create an assistant message
3. Inside the message, we draw a Matplotlib chart
4. The chart is displayed **inside the chat bubble**
```python
import matplotlib.pyplot as plt
if prompt.value:
# user message
user_msg = mr.Message(prompt.value, role="user")
chat.add(user_msg)
# assistant message
response_msg = mr.Message(
"**Matplotlib** plot based on sales data",
role="assistant",
emoji="🤖",
)
with response_msg:
# create figure
fig, ax = plt.subplots(figsize=(6, 3))
ax.plot(sales_df["date"], sales_df["total_sales"], marker="o")
ax.set_xlabel("Date")
ax.set_ylabel("Total Sales")
ax.set_title("Total Sales Over Time")
fig.tight_layout()
# important: close the figure
plt.close(fig)
# display figure inside the chat message
display(fig)
chat.add(response_msg)
```
## Why this works
[Section titled “Why this works”](#why-this-works)
* `Message` can display text, markdown, HTML, charts and tables.
* Everything inside `with response_msg:` is rendered in the response message.
* `display(fig)` shows the Matplotlib figure
* `plt.close(fig)` prevents extra empty space and duplicated plots
# Basic echo chat web app in Python
> Create a basic echo chat web application in Python with the Mercury framework. The chat will accept a user prompt and respond with the same message. You will learn how to use the Chat, ChatInput, and Message widgets from Mercury. This is a good start for building more advanced chat apps.
Have you ever heard an echo in the mountains, a cave, or a tunnel? An echo is a simple response: it repeats your voice back to you.
In this tutorial, we will build a simple **Echo Chat Bot**. It will respond with the same text that the user typed.
You will learn how to use these Mercury widgets:
* `Chat` (`/docs/chat/chat/`)
* `ChatInput` (`/docs/chat/chatinput/`)
* `Message` (`/docs/chat/message/`)
Below is the screenshot of echo bot developed in Python notebook. In the left there is notebook code, in the right there is live app preview:

The live web app:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/echo-chat.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/echo-chat?no-navbar)
## 1. Import Mercury
[Section titled “1. Import Mercury”](#1-import-mercury)
In the first notebook cell, import Mercury:
```python
import mercury as mr
```
We will use `mr` alias to create all widgets.
## 2. Create the chat area
[Section titled “2. Create the chat area”](#2-create-the-chat-area)
In the next cell, create a place where all messages will be displayed:
```python
chat = mr.Chat()
```
Yes, it is that simple :)
## 3. Add an input for the user prompt
[Section titled “3. Add an input for the user prompt”](#3-add-an-input-for-the-user-prompt)
Now we need `ChatInput` to let the user type a message:
```python
prompt = mr.ChatInput()
```
When user enters the text and send it, this automatically re-execute cells below.
## 4. Echo the message back
[Section titled “4. Echo the message back”](#4-echo-the-message-back)
In the next cell, we will:
1. Create a message from the user input
2. Add it to the chat
3. Create the assistant response (`Echo: ...`)
4. Add it to the chat
```python
if prompt.value:
# user message
user_msg = mr.Message(prompt.value, role="user")
chat.add(user_msg)
# assistant message (echo)
response_msg = mr.Message(
f"Echo: {prompt.value}",
role="assistant",
emoji="🤖",
)
chat.add(response_msg)
```
## How it works
[Section titled “How it works”](#how-it-works)
* `prompt.value` contains the latest user input.
* `mr.Message(..., role="user")` creates a message from the user.
* `mr.Message(..., role="assistant")` creates a message from the bot.
* `chat.add(...)` appends messages to the chat view.
## Next steps
[Section titled “Next steps”](#next-steps)
This echo bot is a great starting point. Next you can:
* Replace the echo response with an LLM call (OpenAI, Ollama, OpenRouter, Anthropic, Mistral, Bielik, etc.).
* Add system prompts and tools for an agent-like chat.
* Build interactive responses with charts and tables.
# Dark Theme Altair Charts
> Build an interactive Mercury app with beautiful dark-themed Altair visualizations.
This tutorial shows how to create a Mercury web app with interactive, dark-themed Altair charts.
Altair is a great Python package for creating interactive visualizations. It works very well in notebooks, and with Mercury you can turn the notebook into a web app.
When your Mercury app uses a dark style, it is good to make your charts dark too. A bright chart on a dark page can look out of place. With Altair, you can apply a dark theme and keep the whole app visually consistent.
## What You Will Build
[Section titled “What You Will Build”](#what-you-will-build)
You will build a single-notebook Mercury app with:
* a dropdown to switch between three chart types,
* a checkbox to turn the dark theme on or off,
* interactive Altair charts,
* a clean dark style that works well in Mercury.

## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
Install the required packages:
```bash
pip install mercury altair vega_datasets pandas
```
## 1. Import Packages
[Section titled “1. Import Packages”](#1-import-packages)
Start with the imports:
```python
import mercury as mr
import altair as alt
from vega_datasets import data
import pandas as pd
```
We use:
* `mercury` for widgets and turning the notebook into a web app,
* `altair` for charts,
* `vega_datasets` for example datasets,
* `pandas` for simple data preparation.
## 2. Add Mercury Widgets
[Section titled “2. Add Mercury Widgets”](#2-add-mercury-widgets)
Now add two Mercury widgets.
The first widget lets the user select which chart should be displayed.
The second widget lets the user turn the dark theme on or off.
```python
# Mercury widget: choose which chart to display
chart_select = mr.Select(
value="Scatter: Horsepower vs MPG",
choices=["Scatter: Horsepower vs MPG", "Line: Stock Prices", "Bar: Seattle Precipitation"],
label="Choose Chart",
)
# Mercury widget: toggle dark theme on/off
use_dark = mr.CheckBox(value=True, label="Dark Theme")
```
The `mr.Select` widget creates a dropdown.
The `mr.CheckBox` widget creates a checkbox.
When the user changes a widget, Mercury reruns the notebook and updates the output.
## 3. Apply or Remove the Dark Theme
[Section titled “3. Apply or Remove the Dark Theme”](#3-apply-or-remove-the-dark-theme)
Next, use the checkbox value to decide which Altair theme should be active.
```python
# Apply or remove theme based on checkbox
if use_dark.value:
alt.theme.enable("dark")
else:
alt.theme.enable("default")
```
When the checkbox is selected, Altair uses the dark theme.
When the checkbox is not selected, Altair uses the default theme.
This makes it easy to compare the dark and light chart styles directly in the Mercury app.
## 4. Create a Scatter Plot
[Section titled “4. Create a Scatter Plot”](#4-create-a-scatter-plot)
The first chart shows the relationship between horsepower and miles per gallon.
```python
# Chart 1 — Scatter plot
cars = data.cars()
scatter = (
alt.Chart(cars)
.mark_circle(size=70, opacity=0.85)
.encode(
x=alt.X("Horsepower:Q", title="Horsepower"),
y=alt.Y("Miles_per_Gallon:Q", title="Miles per Gallon"),
color=alt.Color("Origin:N", legend=alt.Legend(title="Origin")),
tooltip=["Name", "Horsepower", "Miles_per_Gallon", "Origin"],
)
.properties(title="Horsepower vs Miles per Gallon", width=600, height=350)
.interactive()
)
```
This chart is interactive. You can zoom and move around the plot.
The tooltip shows more information when the user moves the mouse over a point.
## 5. Create a Line Chart
[Section titled “5. Create a Line Chart”](#5-create-a-line-chart)
The second chart shows stock prices over time.
```python
# Chart 2 — Multi-line stock chart
stocks = data.stocks()
line = (
alt.Chart(stocks)
.mark_line(strokeWidth=2)
.encode(
x=alt.X("date:T", title="Date"),
y=alt.Y("price:Q", title="Price (USD)"),
color=alt.Color("symbol:N", legend=alt.Legend(title="Stock")),
tooltip=["symbol", "date", "price"],
)
.properties(title="Stock Prices Over Time", width=600, height=350)
.interactive()
)
```
The line chart is also interactive. It is useful for exploring time series data.
## 6. Create a Bar Chart
[Section titled “6. Create a Bar Chart”](#6-create-a-bar-chart)
The third chart shows average monthly precipitation in Seattle.
First, load the weather dataset and prepare the month column:
```python
# Chart 3 — Bar chart of monthly precipitation
seattle = data.seattle_weather()
seattle["month"] = pd.to_datetime(seattle["date"]).dt.strftime("%b")
month_order = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
monthly_precip = seattle.groupby("month", as_index=False)["precipitation"].mean()
```
Then create the bar chart:
```python
bar = (
alt.Chart(monthly_precip)
.mark_bar(cornerRadiusTopLeft=3, cornerRadiusTopRight=3)
.encode(
x=alt.X("month:N", sort=month_order, title="Month"),
y=alt.Y("precipitation:Q", title="Avg Precipitation (mm)"),
color=alt.Color(
"precipitation:Q",
scale=alt.Scale(scheme="blues"),
legend=None,
),
tooltip=["month", alt.Tooltip("precipitation:Q", format=".2f")],
)
.properties(title="Seattle: Average Monthly Precipitation", width=600, height=350)
)
```
The bar chart uses rounded top corners and a blue color scale.
Small styling details like this make the chart look more modern.
## 7. Display the Selected Chart
[Section titled “7. Display the Selected Chart”](#7-display-the-selected-chart)
Now create a dictionary with all charts.
The selected value from the dropdown decides which chart should be displayed.
```python
# Display selected chart
charts = {
"Scatter: Horsepower vs MPG": scatter,
"Line: Stock Prices": line,
"Bar: Seattle Precipitation": bar,
}
charts[chart_select.value]
```
That is all.
Mercury reruns the notebook when the user changes the dropdown or checkbox, so the app always displays the selected chart with the selected theme.
We can preview the web app in the Jupyter Lab or MLJAR Studio:

## 8. Run the App
[Section titled “8. Run the App”](#8-run-the-app)
Save the notebook as:
```bash
altair-dark-theme.ipynb
```
Then run it with Mercury:
```bash
mercury altair-dark-theme.ipynb
```
Open the app in your browser.
You will see the Mercury widgets in the sidebar and the selected Altair chart in the main area.
## Why Dark Charts Look Good in Mercury
[Section titled “Why Dark Charts Look Good in Mercury”](#why-dark-charts-look-good-in-mercury)
Dark charts are useful when the rest of the app also uses a dark style.
They make the app look more consistent and polished.
This is useful for:
* dashboards,
* reports,
* monitoring apps,
* machine learning demos,
* internal tools,
* presentations.
The best part is that you do not need complicated code. You can keep your notebook simple and still create a nice web app.
## Summary
[Section titled “Summary”](#summary)
In this tutorial, you created a Mercury app with interactive Altair charts.
You learned how to:
* add Mercury widgets,
* switch between different charts,
* toggle the Altair theme,
* create scatter, line, and bar charts,
* display the selected chart in a Mercury web app.
Altair gives you interactive charts, and Mercury turns your notebook into a web app.
Together, they make it easy to build beautiful data apps directly from Python notebooks.
# How to Style Indicators
> Learn how to customize the visual style of Indicator widgets using variants and explicit colors in Mercury.
Indicators are perfect for highlighting key metrics in your Mercury web app. While the default styling is clean and minimal, you have full control over their appearance to match your branding or alert states.
## 1. Default Built-in Indicator
[Section titled “1. Default Built-in Indicator”](#1-default-built-in-indicator)
To create a basic indicator, simply pass a `value` and a `label`. By default, the indicator provides a neutral card.

```python
import mercury as mr
mr.Indicator(
value="1,024",
label="Total Active Users"
)
```
## 2. Built-in Variant Styles
[Section titled “2. Built-in Variant Styles”](#2-built-in-variant-styles)
You can quickly apply pre-built themes using semantic variant parameters such as `"success"`, `"warning"`, or `"danger"`.

```python
import mercury as mr
ind1 = mr.Indicator(
value="98%",
label="ACCURACY",
variant="success"
)
ind2 = mr.Indicator(
value="340ms",
label="LATENCY",
variant="warning"
)
ind3 = mr.Indicator(
value="$12k",
label="REVENUE",
variant="danger"
)
```
## 3. Custom Styled Indicators
[Section titled “3. Custom Styled Indicators”](#3-custom-styled-indicators)
For complete control over the widget’s appearance, you can explicitly define hex colors. This overrides variants and allows you to customize:
* `background_color`
* `accent_color`
* `border_color`
* `label_color`
* `value_color`

```python
import mercury as mr
# 1. Dark Mode / Cyberpunk
ind_dark = mr.Indicator(
value="142 IOPS",
label="DISK READ",
background_color="#1e1e2f",
accent_color="#ff007f",
border_color="#ff007f",
value_color="#00ffff",
label_color="#aaaaaa"
)
# 2. Soft Pastel / Elegant
ind_pastel = mr.Indicator(
value="8.4 hrs",
label="AVG SLEEP",
background_color="#fdf6e3",
accent_color="#b58900",
border_color="#eee8d5",
value_color="#657b83",
label_color="#93a1a1"
)
# 3. High Contrast Alert
ind_alert = mr.Indicator(
value="OFFLINE",
label="SERVER STATUS",
background_color="#7f0000",
accent_color="#ff4444",
border_color="#ff0000",
value_color="#ffffff",
label_color="#ffcccc"
)
# 4. Deep Corporate / FinTech
ind_corp = mr.Indicator(
value="$4.2M",
label="Q3 REVENUE",
background_color="#0f172a",
accent_color="#38bdf8",
border_color="#1e293b",
value_color="#f8fafc",
label_color="#94a3b8"
)
# 5. Eco / Nature
ind_eco = mr.Indicator(
value="94 AQI",
label="AIR QUALITY",
background_color="#f0fdf4",
accent_color="#22c55e",
border_color="#bbf7d0",
value_color="#166534",
label_color="#15803d"
)
```
# Create Dark Matplotlib Plots
> Learn how to style Matplotlib plots with dark themes so they look great in Mercury apps.
Matplotlib is one of the most popular Python packages for creating charts. It works great in notebooks and it also works very well in Mercury apps.
When you use a dark theme in your Mercury app, the default Matplotlib plot can look too bright. A white chart on a dark dashboard often breaks the visual style.
The good news is that it is very easy to create dark Matplotlib plots. You can use a built-in Matplotlib style or define your own custom colors.

## 1. Install Dependencies
[Section titled “1. Install Dependencies”](#1-install-dependencies)
First, install Mercury, Matplotlib, and NumPy.
```python
# pip install mercury matplotlib numpy
```
Then import the packages:
```python
import mercury as mr
import matplotlib.pyplot as plt
import matplotlib
import numpy as np
```
## 2. Use a Built-in Dark Style
[Section titled “2. Use a Built-in Dark Style”](#2-use-a-built-in-dark-style)
Matplotlib has built-in styles. One of them is called `"dark_background"`.
It changes the plot background, text colors, axes, and grid so the chart looks good on a dark background.
In Mercury, we can also add widgets to make the chart interactive.
```python
import mercury as mr
import matplotlib.pyplot as plt
import numpy as np
# Select chart type
chart_type = mr.Select(
value="Line",
choices=["Line", "Bar", "Scatter"],
label="Chart Type"
)
# Select Matplotlib style
dark_style = mr.Select(
value="dark_background",
choices=["dark_background", "Solarize_Light2"],
label="Plot Style"
)
```
Now we can generate sample data and display the chart.
```python
# Generate sample data
x = np.linspace(0, 10, 100)
y1 = np.sin(x)
y2 = np.cos(x)
y3 = np.sin(x) * np.cos(x)
# Apply selected style
with plt.style.context(dark_style.value):
fig, ax = plt.subplots(figsize=(10, 5))
if chart_type.value == "Line":
ax.plot(x, y1, color="#7C3AED", linewidth=2, label="sin(x)")
ax.plot(x, y2, color="#38BDF8", linewidth=2, label="cos(x)")
ax.plot(x, y3, color="#34D399", linewidth=2, label="sin(x)cos(x)")
elif chart_type.value == "Bar":
categories = ["A", "B", "C", "D", "E"]
values = np.random.randint(10, 100, 5)
colors = ["#7C3AED", "#38BDF8", "#34D399", "#F59E0B", "#EC4899"]
ax.bar(categories, values, color=colors)
elif chart_type.value == "Scatter":
scatter_x = np.random.randn(100)
scatter_y = np.random.randn(100)
ax.scatter(scatter_x, scatter_y, color="#7C3AED", alpha=0.6, s=50)
ax.set_title(f"Dark Theme {chart_type.value} Chart", fontsize=14, pad=15)
ax.set_xlabel("X Axis")
ax.set_ylabel("Y Axis")
if chart_type.value == "Line":
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
```
This is the simplest way to make Matplotlib plots match a dark Mercury app.
## 3. Create a Custom Dark Theme
[Section titled “3. Create a Custom Dark Theme”](#3-create-a-custom-dark-theme)
The built-in style is a great start, but sometimes you want full control over colors.
For example, you might want to use your own background color, grid color, text color, or brand colors.
Matplotlib allows this with `matplotlib.rc_context()`.
```python
custom_dark = {
"figure.facecolor": "#0f0f0f",
"axes.facecolor": "#1a1a1a",
"axes.edgecolor": "#333333",
"axes.labelcolor": "#ffffff",
"text.color": "#ffffff",
"xtick.color": "#6B7280",
"ytick.color": "#6B7280",
"grid.color": "#333333",
"grid.alpha": 0.5,
}
```
Now we can use this style only for selected plots.
```python
with matplotlib.rc_context(custom_dark):
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# Plot 1 - Line chart
axes[0].plot(x, y1, color="#A78BFA", linewidth=2)
axes[0].fill_between(x, y1, alpha=0.2, color="#A78BFA")
axes[0].set_title("Custom Dark Line Chart")
axes[0].grid(True)
# Plot 2 - Bar chart
categories = ["Mon", "Tue", "Wed", "Thu", "Fri"]
values = [23, 45, 32, 67, 54]
axes[1].bar(
categories,
values,
color=["#A78BFA", "#38BDF8", "#34D399", "#F59E0B", "#EC4899"]
)
axes[1].set_title("Custom Dark Bar Chart")
axes[1].grid(True, axis="y")
plt.tight_layout()
plt.show()
```
The custom theme gives you more control. You can match the plot with your Mercury app theme, your company colors, or your dashboard design.

## 4. Why Dark Plots Look Better in Dark Apps
[Section titled “4. Why Dark Plots Look Better in Dark Apps”](#4-why-dark-plots-look-better-in-dark-apps)
When your Mercury app uses a dark style, dark plots make the whole app feel consistent.
It is especially useful for:
* dashboards with key metrics,
* data science reports,
* monitoring apps,
* machine learning demos,
* presentations,
* apps used in low-light environments.
Small visual changes can make your app look much more polished.
## Summary
[Section titled “Summary”](#summary)
You can create dark Matplotlib plots in Mercury in two simple ways:
1. Use the built-in Matplotlib style `"dark_background"` for a quick dark theme.
2. Use `matplotlib.rc_context()` to define your own custom dark colors.
Both methods are beginner-friendly and work well with Mercury widgets.
If your Mercury app uses a dark theme, it is worth styling your plots too. The app will look cleaner, more professional, and easier to read.
# Streaming LLM Chatbot
> Learn how to stream tokens from a local Ollama model into a Mercury Chat widget, so your chatbot replies in real time.
In this tutorial we will build a simple chatbot web app that **streams the response token-by-token** (as the model generates it). This gives a nice **the assistant is typing** feeling.
We will use only open-source tools:
* **Ollama** to run the model locally
* **GPT-OSS 20B** as the model (you can switch to any Ollama model)
* **Python** programming language
* **Mercury** to turn the notebook into a web app
The full notebook code is vailable in our [Github repository](https://github.com/mljar/mercury/blob/main/docs/notebooks/ollama-streaming.ipynb).

## 1. Install and run Ollama
[Section titled “1. Install and run Ollama”](#1-install-and-run-ollama)
Install Ollama using the official quickstart:
In this example I’m using **GPT-OSS 20B**.
To download and start the model, run:
```bash
ollama run gpt-oss:20b
```
Donwload will take few minutes, depending on your internet connection. You are ready to use the model in the terminal:

## 2. Install Python packages
[Section titled “2. Install Python packages”](#2-install-python-packages)
We need two packages:
* `ollama` (Python client)
* `mercury` (widgets + app runtime)
Install them:
```bash
pip install ollama mercury
```
Now import them in the first cell:
```python
import ollama
import mercury as mr
```
Note
We use `mr` as alias for `mercury` package, so we can easily access any widget from the framework.
## 3. Create storage for the conversation
[Section titled “3. Create storage for the conversation”](#3-create-storage-for-the-conversation)
We will keep the whole conversation in a list called `messages`. Ollama expects messages in the same format as many chat APIs: a list of dicts with `role` and `content`.
```python
# list with all user and assistant messages
messages = []
```
> Why do we need this list?
Because each new response should include the conversation history, so the model has context.
## 4. Add UI widgets: Chat + ChatInput
[Section titled “4. Add UI widgets: Chat + ChatInput”](#4-add-ui-widgets-chat--chatinput)
### Chat widget (message display)
[Section titled “Chat widget (message display)”](#chat-widget-message-display)
We use the [`Chat`](/docs/chat/chat/) widget to display the conversation. The `placeholder` is shown before the first message appears.
```python
# place to display messages
chat = mr.Chat(placeholder="💬 Start conversation")
```
### ChatInput widget (prompt box)
[Section titled “ChatInput widget (prompt box)”](#chatinput-widget-prompt-box)
We also need an input at the bottom of the app. We use [`ChatInput`](/docs/chat/chatinput/).
```python
# user input
prompt = mr.ChatInput()
```
Note
`Chat` and `ChatInput` are **not in the same cell** on purpose.
Mercury places all widgets created in the same code cell in the same position in the layout. This is convenient most of the time, but here we want:
* the chat content in the main area
* the input at the bottom

## 5. Stream the response from Ollama into the UI
[Section titled “5. Stream the response from Ollama into the UI”](#5-stream-the-response-from-ollama-into-the-ui)
Now the fun part 😊
Mercury automatically re-executes notebook cells when a widget changes. So when the user submits text in `ChatInput`, `prompt.value` becomes non-empty and the next cell runs.
Here is the full streaming cell:
```python
if prompt.value:
# create user message
usr_msg = mr.Message(markdown=prompt.value, role="user")
# display user message in the chat
chat.add(usr_msg)
# save in messages list (history for Ollama)
messages += [{'role': 'user', 'content': prompt.value}]
# call local LLM with streaming enabled
stream = ollama.chat(
model='gpt-oss:20b',
messages=messages,
stream=True,
)
# create assistant message (empty at the beginning)
ai_msg = mr.Message(role="assistant", emoji="🤖")
# display assistant message in the chat
chat.add(ai_msg)
# stream the response token-by-token
content = ""
for chunk in stream:
ai_msg.append_markdown(chunk.message.content)
content += chunk.message.content
# save assistant response in history
messages += [{'role': 'assistant', 'content': content}]
```
Notebook and app preview:

### Step-by-step explanation (what happens here?)
[Section titled “Step-by-step explanation (what happens here?)”](#step-by-step-explanation-what-happens-here)
Let’s go through the code slowly.
#### 1) Check if the user submitted a prompt
[Section titled “1) Check if the user submitted a prompt”](#1-check-if-the-user-submitted-a-prompt)
```python
if prompt.value:
```
`prompt.value` contains the text from `ChatInput`. If it is empty, we do nothing.
#### 2) Add the user message to the UI + history
[Section titled “2) Add the user message to the UI + history”](#2-add-the-user-message-to-the-ui--history)
```python
usr_msg = mr.Message(markdown=prompt.value, role="user")
chat.add(usr_msg)
messages += [{'role': 'user', 'content': prompt.value}]
```
We do three things:
* create [`Message`](/docs/chat/message/) object, with `markdown` and `role`
* show the message in the `Chat` widget (`chat.add(...)`)
* store it in `messages` so the model sees the full conversation next time
#### 3) Call Ollama with `stream=True`
[Section titled “3) Call Ollama with stream=True”](#3-call-ollama-with-streamtrue)
```python
stream = ollama.chat(
model='gpt-oss:20b',
messages=messages,
stream=True,
)
```
This is the key: `stream=True` makes Ollama return an iterator. Instead of one big response, we receive many small chunks.
#### 4) Create an empty assistant message in the chat
[Section titled “4) Create an empty assistant message in the chat”](#4-create-an-empty-assistant-message-in-the-chat)
```python
ai_msg = mr.Message(role="assistant", emoji="🤖")
chat.add(ai_msg)
```
We add the assistant message *before* we have any text. Now we have a “container” that we can update as tokens arrive.
#### 5) Append tokens as they come
[Section titled “5) Append tokens as they come”](#5-append-tokens-as-they-come)
```python
for chunk in stream:
ai_msg.append_markdown(chunk.message.content)
content += chunk.message.content
```
For each chunk:
* `append_markdown(...)` updates the UI immediately
* we also keep `content` as a normal string, so we can store the final answer
#### 6) Save the assistant response in history
[Section titled “6) Save the assistant response in history”](#6-save-the-assistant-response-in-history)
```python
messages += [{'role': 'assistant', 'content': content}]
```
This step is important for multi-turn chat. Without it, the next prompt would not include the assistant’s last reply.
## 6. Run as web application
[Section titled “6. Run as web application”](#6-run-as-web-application)
Please start `mercury` server application by running the following command:
```bash
mercury
```
By default, the application will detect all notebook files in the current directory (files with `*.ipynb` extension) and serve them as web apps. If you prefer, you can point Mercury at a different base directory with `--working-dir`. The code won’t be displayed. After opening the `mercury` website you will get a view of all notebooks, please just click on the app to open it.

## Notes and tips
[Section titled “Notes and tips”](#notes-and-tips)
* You can switch the model name to any Ollama model you have installed.
* For better answers, keep the `messages` list (conversation history). If you remove it, the chatbot becomes “single-turn” (no memory).
* If you want to clear the conversation, you can add a button that resets `messages` and the chat.
Have fun building! 🤖🎉
# Streaming with Thinking Output
> Learn how to stream both reasoning ("thinking") and final answers from a local Ollama model into a Mercury chat web app.
In this example we build a chatbot that not only streams the answer — but also streams the model’s **thinking process** in real time.
This creates an experience where users can see how the model reasons step by step before producing the final answer.
We use:
* **Ollama** to run the model locally
* **GPT-OSS 20B** as the model
* **Python**
* **Mercury** to turn the notebook into a web app
Full notebook:\
## See the basic streaming chatbot example
[Section titled “See the basic streaming chatbot example”](#see-the-basic-streaming-chatbot-example)
If you haven’t already, you can also check our **streaming LLM chatbot example**, which shows how to stream the **final answer tokens**:
👉 (Streaming LLM chatbot)\[/examples/ollama/build-streaming-llm-chatbot/)

The code that is the same as in streaming example:
```python
import ollama
import mercury as mr
```
```python
# list with all user and assistant messages
messages = []
```
```python
# place to display messages
chat = mr.Chat(placeholder="💬 Start conversation")
```
```python
# user input
prompt = mr.ChatInput()
```
## What is different from a normal streaming chatbot?
[Section titled “What is different from a normal streaming chatbot?”](#what-is-different-from-a-normal-streaming-chatbot)
In a standard chatbot, we stream only the **final answer**.
Here, the model sends two types of tokens:
* `thinking` - internal reasoning steps
* `content` - final user-facing answer
We will display both in the chat UI. The assistant message will look like:
```plaintext
Thinking: ...
Answer: ...
```
The response message with thinking and final content:

### The key idea
[Section titled “The key idea”](#the-key-idea)
The whole notebook is the same as the basic streaming chatbot example, except **the last code cell**.
This cell separates:
* reasoning tokens
* final answer tokens
and streams them differently into the chat.
## Streaming thinking + answer
[Section titled “Streaming thinking + answer”](#streaming-thinking--answer)
```python
if prompt.value:
# create user message
usr_msg = mr.Message(markdown=prompt.value, role="user")
chat.add(usr_msg)
messages += [{'role': 'user', 'content': prompt.value}]
# call local LLM
stream = ollama.chat(
model='gpt-oss:20b',
messages=messages,
stream=True,
)
# create assistant message
ai_msg = mr.Message(role="assistant", emoji="🤖")
chat.add(ai_msg)
# stream thinking and answer separately
thinking, content = "", ""
for chunk in stream:
if chunk.message.thinking:
if thinking == "":
ai_msg.append_markdown("Thinking: ")
thinking += chunk.message.thinking
ai_msg.append_markdown(chunk.message.thinking)
elif chunk.message.content:
if content == "":
ai_msg.append_markdown("\n\nAnswer: ")
content += chunk.message.content
ai_msg.append_markdown(chunk.message.content)
# save assistant response
messages += [{'role': 'assistant', 'thinking': thinking, 'content': content}]
```
## How this works
[Section titled “How this works”](#how-this-works)
### 1. The model sends two streams
[Section titled “1. The model sends two streams”](#1-the-model-sends-two-streams)
Each chunk from Ollama may contain:
* `chunk.message.thinking`
* `chunk.message.content`
We check which one is present.
### 2. When thinking appears
[Section titled “2. When thinking appears”](#2-when-thinking-appears)
```python
if chunk.message.thinking:
```
We:
* print `"Thinking: "` only once
* append new reasoning tokens as they arrive
This gives a live reasoning effect.
### 3. When the final answer starts
[Section titled “3. When the final answer starts”](#3-when-the-final-answer-starts)
```python
elif chunk.message.content:
```
We:
* insert a section header `"Answer: "`
* stream the final text
### 4. Why store both thinking and content?
[Section titled “4. Why store both thinking and content?”](#4-why-store-both-thinking-and-content)
```python
messages += [{'role': 'assistant', 'thinking': thinking, 'content': content}]
```
This keeps the full conversation history, including reasoning, so the model can use previous steps in later turns.
## Notes
[Section titled “Notes”](#notes)
* Not all models support streaming reasoning tokens.
* You can hide the “Thinking” section if you want a cleaner UI.
You just built a **transparent AI chatbot** that shows its reasoning live 💃🚀
# Chatbot with Tool Calling
> A practical guide to implementing LLM tool calling using Ollama, GPT models, and Python. Stream reasoning, execute functions, and turn your notebook into a web application.
In this example, we build a chatbot that can do more than just generate text — it can **use tools**. Normally, a language model can only answer based on what it already knows. It cannot check the weather, look up fresh data, or run your code. **Tool calling changes that**. It allows the model to decide that it needs outside information, ask your program to run a function, receive the result, and then use that result to produce a better, more accurate answer. This makes the chatbot feel less like a text generator and more like a real assistant that can interact with the world around it. In this tutorial, you will learn how to build this step by step using Ollama, Python, and Mercury, while also streaming the model’s thinking and final response in real time.
📓 Full notebook code is [in our GitHub repository](https://github.com/mljar/mercury/blob/main/docs/notebooks/ollama-tool-calling.ipynb).

## What You Will Learn
[Section titled “What You Will Learn”](#what-you-will-learn)
In this guide you will learn how to:
* Stream AI responses word by word
* Stream the **model’s thinking** separately
* Let the model use tools
* Build a simple chat interface
* Turn everything into a web app
## 1. Install and Run Ollama
[Section titled “1. Install and Run Ollama”](#1-install-and-run-ollama)
Install Ollama using the official guide: [docs.ollama.com/quickstart](https://docs.ollama.com/quickstart)
We use **GPT-OSS 20B**.
Download and start the model:
```bash
ollama run gpt-oss:20b
```
After download, the model runs locally on your machine and you can use it in terminal. But let’s move further!

## 2. Install Python Packages
[Section titled “2. Install Python Packages”](#2-install-python-packages)
We need two packages:
* `ollama` — to talk to the model
* `mercury` — to build the chat app
Both packages are open source and easy to install:
```bash
pip install ollama mercury
```
Import them in the first code cell:
```python
import ollama
import mercury as mr
```
We are ready to code more :)
## 3. Create a Tool the Model Can Use
[Section titled “3. Create a Tool the Model Can Use”](#3-create-a-tool-the-model-can-use)
We define a simple Python function. The model can call this like a **tool**.
```python
def get_temperature(city: str) -> str:
"""Get the current temperature for a city"""
temperatures = {
'New York': '22°C',
'London': '15°C'
}
return temperatures.get(city, 'Unknown')
```
This function:
* Takes a city name
* Returns a temperature as string
Note
This is simple function just to show you how tool calling working with LLMs.
In real life such function will connect with database, REST API or something more sophisticated.
## 4. Build the Chat Interface
[Section titled “4. Build the Chat Interface”](#4-build-the-chat-interface)
We will store our conversation in the list `messages`. It is used to provide context to local LLM model, without it model will not remember about dialog.
```python
messages = []
```
Let’s clreate a [`Chat`](/docs/chat/chat/) widget for displaying messages in the next code cell:
```python
chat = mr.Chat(placeholder="💬 Start conversation")
```
User will provide prompts with [`ChatInput`](/docs/chat/chatinput/) widget.
```python
prompt = mr.ChatInput()
```
Chat interface components:
* `chat` shows messages on screen
* `prompt` is where the user types
Note
It is important to put widgets `chat` and `prompt` in the separate code cells. Mercury is placing widgets on cell basis in the app. It means you can’t mix widgets from different position in one cell.
It is not a bug. It is a feature.
## 5. Stream Thinking and Answer from the Model
[Section titled “5. Stream Thinking and Answer from the Model”](#5-stream-thinking-and-answer-from-the-model)
This is the main logic. It is called after every new `prompt` from user. This code cell is re-executed for every new input.
```python
if prompt.value:
usr_msg = mr.Message(markdown=prompt.value, role="user")
chat.add(usr_msg)
messages += [{'role': 'user', 'content': prompt.value}]
stream = ollama.chat(
model='gpt-oss:20b',
messages=messages,
tools=[get_temperature],
stream=True,
)
ai_msg = mr.Message(role="assistant", emoji="🤖")
chat.add(ai_msg)
thinking, content = "", ""
tool_calls = []
for chunk in stream:
if chunk.message.thinking:
if thinking == "":
ai_msg.append_markdown("**Thinking:** ")
thinking += chunk.message.thinking
ai_msg.append_markdown(chunk.message.thinking)
elif chunk.message.content:
if content == "":
ai_msg.append_markdown("\n\n**Answer:** ")
content += chunk.message.content
ai_msg.append_markdown(chunk.message.content)
elif chunk.message.tool_calls:
tool_calls.extend(chunk.message.tool_calls)
messages += [{
'role': 'assistant',
'thinking': thinking,
'content': content,
'tool_calls': tool_calls
}]
```
What happens here?
1. User message is created and displayed in the `chat`
2. We call the model with streaming
3. We listen to chunks from the model
4. We separate:
* thinking
* final answer
* tool calls
The screenshot of notebook and app preview:

## 6. 🔍 How the Last Code Piece Works (Tool Execution)
[Section titled “6. 🔍 How the Last Code Piece Works (Tool Execution)”](#6--how-the-last-code-piece-works-tool-execution)
Let’s look closer how we handle tools the model asked to use.
```python
for tool in tool_calls:
if tool.function.name == "get_temperature":
tool_msg = mr.Message(role="tool", emoji="⛅")
chat.add(tool_msg)
result = get_temperature(**tool.function.arguments)
tool_msg.append_markdown("Temperature is " + result)
messages += [{
"role": "tool",
"tool_name": "get_temperature",
"content": result
}]
```
Let’s break this down step by step:
### 1️⃣ Loop through tool calls
[Section titled “1️⃣ Loop through tool calls”](#1️⃣-loop-through-tool-calls)
```python
for tool in tool_calls:
```
During streaming, the model may say:
> “I want to call a tool.”
Those requests are stored in `tool_calls`. Now we go through each one.
### 2️⃣ Check which tool the model wants
[Section titled “2️⃣ Check which tool the model wants”](#2️⃣-check-which-tool-the-model-wants)
```python
if tool.function.name == "get_temperature":
```
The model tells us the name of the function. We check if it matches our function.
### 3️⃣ Show tool message in the chat
[Section titled “3️⃣ Show tool message in the chat”](#3️⃣-show-tool-message-in-the-chat)
```python
tool_msg = mr.Message(role="tool", emoji="⛅")
chat.add(tool_msg)
```
We create a message from the **tool**, so the user sees that a tool is being used.
### 4️⃣ Run the function in Python
[Section titled “4️⃣ Run the function in Python”](#4️⃣-run-the-function-in-python)
```python
result = get_temperature(**tool.function.arguments)
```
Very important part.
* `tool.function.arguments` contains data like:
```json
{"city": "London"}
```
* `**tool.function.arguments` means: unpack arguments So this becomes:
```python
get_temperature(city="London")
```
Now Python runs the function and returns `"15°C"`.
### 5️⃣ Show tool result
[Section titled “5️⃣ Show tool result”](#5️⃣-show-tool-result)
```python
tool_msg.append_markdown("Temperature is " + result)
```
The user now sees:
> Temperature is 15°C
### 6️⃣ Add tool result to conversation history
[Section titled “6️⃣ Add tool result to conversation history”](#6️⃣-add-tool-result-to-conversation-history)
```python
messages += [{
"role": "tool",
"tool_name": "get_temperature",
"content": result
}]
```
This is very important.
We tell the model:
👉 “The tool ran.” 👉 “Here is the result.”
Now the model can continue and use this information in its final answer.
## Why This Pattern Is Powerful
[Section titled “Why This Pattern Is Powerful”](#why-this-pattern-is-powerful)
Normal chatbot:
```plaintext
User → Model → Answer
```
This system:
```plaintext
User → Model Thinking → Tool Call → Tool Result → Final Answer
```
This is the base for:
* AI agents
* Smart assistants
* Tool-using AI systems
## Turn It Into a Web App
[Section titled “Turn It Into a Web App”](#turn-it-into-a-web-app)
Running the notebook as web app is simple as starting a Mercury Server with command:
```bash
mercury
```
Mercury will detect all `*.ipynb` files and serve them as web applications.
## Summary
[Section titled “Summary”](#summary)
Great job — you have just built a chatbot that behaves more like a real assistant than a simple text generator. Instead of only guessing answers from what the model already knows, your chatbot can decide to use a tool, run a Python function, and use the result to give a better and more accurate response. At the same time, users can watch how the model thinks and see the final answer appear step by step, which makes the whole system more transparent and easier to understand.
You also learned how to connect a local model running with **Ollama** to a Python application, how to pass conversation history, how tool calls are detected and executed, and how to show everything in an interactive interface using **Mercury**. By turning the notebook into a web app, you transformed a small code example into something people can actually use.
This pattern — model thinking, tool calling, tool result, and final answer — is the core idea behind modern AI assistants and agents. Once you understand this flow, you can connect models to databases, APIs, or your own business logic, and build systems that do real work, not just chat.
# Excel Cleaner
> Learn how to build a web app that opens any Excel file, shows per-sheet stats, lets the user pick a sheet, apply cleaning operations, and download the result as a CSV.
In this tutorial we will build an **Excel cleaner** — a web app where you upload any `.xlsx` file, browse its sheets, apply common cleaning operations, and download the result as a CSV.

No code, no Excel macros, no manual find-and-replace. Just upload, click, and download.
We will use:
* **pandas** for reading the Excel file and applying cleaning operations,
* **Mercury** to turn the notebook into a web app with file upload, sheet selection, checkboxes, and a download button.
The full notebook code is available in our [GitHub repository](https://github.com/mljar/mercury-notebook-apps/tree/main/excel-cleaner).
You can also try the [live demo](https://utility-apps.ismvp.org/mercury/excel-cleaner).
## What the app shows
[Section titled “What the app shows”](#what-the-app-shows)
After the user uploads an Excel file, the app displays:
1. **Sheets overview** — a table listing every sheet with row count, column count, empty cells, and duplicate rows.
2. **Sheet selector** — a dropdown to pick which sheet to inspect and clean.
3. **Original data preview** — the raw content of the chosen sheet.
4. **Cleaning options** — checkboxes in the sidebar to normalize column names and/or remove duplicate rows.
5. **Results table** — the cleaned data, updated live as the user toggles options.
6. **Download button** — exports the cleaned sheet as a CSV file.
## 1. Install packages
[Section titled “1. Install packages”](#1-install-packages)
```bash
pip install mercury pandas openpyxl
```
Note
`openpyxl` is required by pandas to read `.xlsx` files. Without it, `pd.read_excel` will fail with a missing-dependency error.
## 2. Import libraries
[Section titled “2. Import libraries”](#2-import-libraries)
```python
import mercury as mr
from IPython.display import clear_output
```
`clear_output` from IPython is used to hide empty cells when no file has been uploaded yet, keeping the app clean on first load.
## 3. Add the file upload widget
[Section titled “3. Add the file upload widget”](#3-add-the-file-upload-widget)
```python
file = mr.UploadFile(label="Upload your Excel file", accept=".xlsx")
```
[`UploadFile`](/docs/widgets/uploadfile/) renders a file picker in the app sidebar, restricted to `.xlsx` files.

When the user picks a file:
* `file.name` becomes the filename (non-empty string),
* `file.value` contains the raw file bytes.
Mercury automatically re-runs the notebook when the upload changes, so all cells below react immediately.
## 4. Define the cleaning functions
[Section titled “4. Define the cleaning functions”](#4-define-the-cleaning-functions)
Two operations the user will be able to toggle: normalize column names and remove duplicate rows.
### Normalizing column names
[Section titled “Normalizing column names”](#normalizing-column-names)
```python
import re
import unicodedata
def normalize_column_name(column_name):
# Convert to string
column_name = str(column_name)
# Remove accents
column_name = unicodedata.normalize("NFKD", column_name)
column_name = column_name.encode("ascii", "ignore").decode("ascii")
# Convert to lowercase
column_name = column_name.lower()
# Remove extra spaces
column_name = column_name.strip()
# Replace special characters with _
column_name = re.sub(r"[^a-z0-9]+", "_", column_name)
# Remove _ from start and end
column_name = column_name.strip("_")
return column_name
def normalize_column_names(df):
df = df.copy()
df.columns = [normalize_column_name(col) for col in df.columns]
return df
```
This turns something like `"Customer Name (€)"` into `customer_name`. Useful when the Excel file was filled out by hand and column headers contain accents, mixed casing, or punctuation that breaks downstream code.
We will also reuse `normalize_column_name` later for the downloaded filename, so that sheet names with spaces or special characters don’t end up in the CSV name.
### Removing duplicate rows
[Section titled “Removing duplicate rows”](#removing-duplicate-rows)
```python
def remove_duplicate_rows(df):
df_clean = df.copy()
df_clean = df_clean.drop_duplicates()
return df_clean
```
Straightforward pandas — nothing unusual here.
## 5. Read the Excel file and collect per-sheet stats
[Section titled “5. Read the Excel file and collect per-sheet stats”](#5-read-the-excel-file-and-collect-per-sheet-stats)
```python
if file.name is not None:
import pandas as pd
from io import BytesIO
data = BytesIO(file.value)
excel = pd.ExcelFile(data)
sheet_names = excel.sheet_names
sheets_info = []
df_list = []
ready_df = []
for sheet_name in excel.sheet_names:
df = pd.read_excel(data, sheet_name=sheet_name)
df_list.append(df)
ready_df.append(df)
rows = df.shape[0]
columns = df.shape[1]
empty_cells = df.isna().sum().sum()
duplicate_rows = df.duplicated().sum()
sheets_info.append({
"Sheet Name": sheet_name,
"Rows": rows,
"Columns": columns,
"Empty Cells": empty_cells,
"Duplicate Rows": duplicate_rows
})
sheets_info_df = pd.DataFrame(sheets_info)
```
We wrap the raw bytes in `BytesIO` so pandas can read them directly, without saving the file to disk first. Two lists are kept in parallel:
* `df_list` — the original, untouched DataFrames (used for the “before” preview),
* `ready_df` — the working copies that cleaning operations modify.
This way the user can always see what changed.
## 6. Show the welcome message or file title
[Section titled “6. Show the welcome message or file title”](#6-show-the-welcome-message-or-file-title)
```python
if file.name is None:
_ = mr.Markdown("# Upload the Excel file to start")
else:
_ = mr.Markdown(f"# Uploaded file: {file.name}", key='title_md')
```
A simple branch: when nothing has been uploaded yet, show the welcome heading; otherwise show the filename of the uploaded workbook.
`mr.Markdown(...)` renders immediately, so no `display()` wrapper is needed. Assigning its returned widget to `_` prevents IPython from displaying the last expression a second time. The same pattern appears in every conditional cell below.
## 7. Show the sheets overview
[Section titled “7. Show the sheets overview”](#7-show-the-sheets-overview)
```python
if file.name is not None:
sheets_info_table = mr.Table(sheets_info_df, page_size=20, key='sheets-info')
else:
clear_output()
```
[`Table`](/docs/widgets/table/) renders the per-sheet stats as a clean HTML table. `clear_output()` hides the cell entirely when there’s no file yet, so the app doesn’t leave empty gaps on first load.

## 8. Let the user pick a sheet
[Section titled “8. Let the user pick a sheet”](#8-let-the-user-pick-a-sheet)
```python
if file.name is not None:
sheet_select = mr.Select(label="Choose sheet", choices=sheet_names)
else:
clear_output()
```
[`Select`](/docs/widgets/select/) renders a dropdown in the sidebar populated with the sheet names from the uploaded file.
We use `sheet_names.index(sheet_select.value)` later to look up the right DataFrame from `df_list` and `ready_df`.
## 9. Preview the original sheet
[Section titled “9. Preview the original sheet”](#9-preview-the-original-sheet)
```python
if file.name is not None:
_ = mr.Markdown(f"## Sheet: {sheet_select.value}", key='sheet_md')
else:
clear_output()
```
```python
if file.name is not None:
oryginal_data_table = mr.Table(
df_list[sheet_names.index(sheet_select.value)],
page_size=20,
key=f"oryginal-df-{sheet_select.value}"
)
else:
clear_output()
```

Note the dynamic `key` — it includes the sheet name. Mercury uses `key` to identify widgets across re-runs, and varying it per sheet forces the table to fully re-render when the user switches sheets.
## 10. Add the cleaning checkboxes
[Section titled “10. Add the cleaning checkboxes”](#10-add-the-cleaning-checkboxes)
```python
if file.name is not None:
_ = mr.Markdown("### Choose operation", position="sidebar", key='checkboxes')
normalize_col_names_checkbox = mr.CheckBox(
label="Normalize column names",
appearance="box",
key=f"normalize_col_names_checkbox-{sheet_select.value}"
)
remove_duplicate_rows_checkbox = mr.CheckBox(
label="Remove duplicate rows",
appearance="box",
key=f"remove_duplicate_rows_checkbox-{sheet_select.value}"
)
else:
clear_output()
```
[`CheckBox`](/docs/widgets/checkbox/) with `appearance="box"` renders as a tappable card rather than a plain checkbox. `position="sidebar"` keeps the controls grouped with the file upload.
Including `sheet_select.value` in the `key` means the checkboxes reset whenever the user switches to another sheet — exactly what you want, since the cleaning state shouldn’t leak across sheets.

## 11. Apply the selected operations
[Section titled “11. Apply the selected operations”](#11-apply-the-selected-operations)
```python
if file.name is not None:
if normalize_col_names_checkbox.value:
normalize_col_names_checkbox.disabled = True
ready_df[sheet_names.index(sheet_select.value)] = normalize_column_names(
ready_df[sheet_names.index(sheet_select.value)]
)
if remove_duplicate_rows_checkbox.value:
remove_duplicate_rows_checkbox.disabled = True
ready_df[sheet_names.index(sheet_select.value)] = remove_duplicate_rows(
ready_df[sheet_names.index(sheet_select.value)]
)
```
Each operation:
1. Checks if the user ticked the box.
2. Disables the box so it can’t be toggled off mid-pipeline (the operation has already been applied).
3. Updates the working DataFrame in-place for the current sheet.
The disabling step is a small UX touch — once you’ve cleaned the data, you can’t “untick” your way back to the original; you’d need to refresh.
## 12. Show the cleaned results
[Section titled “12. Show the cleaned results”](#12-show-the-cleaned-results)
```python
if file.name is not None:
if normalize_col_names_checkbox.value or remove_duplicate_rows_checkbox.value:
_ = mr.Markdown("## Results", key='results')
edited_data_table = mr.Table(
ready_df[sheet_names.index(sheet_select.value)],
page_size=20,
key=f"results-table-{sheet_select.value}"
)
else:
clear_output()
```
The results table only appears once at least one cleaning option is active. Until then the user just sees the original preview, which avoids visual clutter on first load.

## 13. Add the download button
[Section titled “13. Add the download button”](#13-add-the-download-button)
```python
if file.name is not None:
csv_data = ready_df[sheet_names.index(sheet_select.value)].to_csv(index=False)
```
```python
if file.name is not None:
if normalize_col_names_checkbox.value or remove_duplicate_rows_checkbox.value:
_ = mr.Markdown("### Download edited sheet", position="sidebar", key='download')
mr.Download(
data=csv_data,
filename=f"{normalize_column_name(sheet_select.value)}-edited.csv",
mime="text/csv",
label="Download as CSV",
key=f"csv-download-{sheet_select.value}-{normalize_col_names_checkbox.value}-{remove_duplicate_rows_checkbox.value}"
)
else:
clear_output()
```
[`Download`](/docs/widgets/download/) renders a button in the sidebar that streams `csv_data` as a file when clicked. The cleaned DataFrame is serialized to CSV (without the pandas index).
Two small details worth noting:
* The filename is built from `normalize_column_name(sheet_select.value)` so that a sheet called `"Q1 Sales 2024"` downloads as `q1_sales_2024-edited.csv` rather than something with awkward spaces or accents.
* The `key` includes both checkbox values so the download button refreshes whenever the cleaning pipeline changes — without this, the user might end up downloading stale CSV bytes after toggling an option.

## 14. Run as a web app
[Section titled “14. Run as a web app”](#14-run-as-a-web-app)
Start the Mercury server from the folder containing the notebook:
```bash
mercury
```
Mercury will detect all `*.ipynb` files and serve them as web applications.
## Notes and tips
[Section titled “Notes and tips”](#notes-and-tips)
* This app exports only the **currently selected sheet** as CSV. If you want to export every sheet at once, build a workbook with `pd.ExcelWriter` and offer it with `mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"`.
* The two cleaning operations are intentionally minimal. Easy extensions: trim whitespace from string cells, drop fully empty columns, convert date-like text columns to proper datetimes, fill missing values with a default.
* `pd.read_excel` is slow on large workbooks because it parses every sheet upfront. For files with many sheets, consider lazy-loading: list sheet names first, then read only the sheet the user picks.
* The dynamic `key` pattern (`f"normalize_col_names_checkbox-{sheet_select.value}"`) is the easiest way to reset widget state when a parent selection changes. Use it whenever a downstream widget’s meaning depends on an upstream choice.
* To support older `.xls` files, add `accept=".xlsx,.xls"` to `UploadFile` and install `xlrd` alongside `openpyxl`.
# CSV Dataset Summarizer
> Learn how to build a web app that accepts a CSV upload and instantly shows key dataset stats — row count, missing values, duplicates, column types, and an interactive data preview.
In this tutorial we will build a **CSV dataset summarizer** — a web app where you upload any CSV file and immediately get a structured overview of what’s inside.

No code, no terminal, no pandas knowledge required from the user. Just upload and read.
We will use:
* **pandas** for reading and analysing the CSV,
* **skrub** for the interactive data preview table,
* **Mercury** to turn the notebook into a web app.
The full notebook code is available in our [GitHub repository](https://github.com/mljar/mercury-notebook-apps/tree/main/summarize-dataset).
You can also try the [live demo](https://utility-apps.ismvp.org/mercury/summarize-dataset).
## What the app shows
[Section titled “What the app shows”](#what-the-app-shows)
After the user uploads a CSV, the app displays three sections:
1. **Key indicators** — rows, columns, duplicate rows (with %), missing values (with %).
2. **Column type summary** — a table breaking down how many columns are numeric, categorical, text, datetime, boolean, or constant.
3. **Data preview** — an interactive table with the first 15 rows, powered by skrub’s `TableReport`.
## 1. Install packages
[Section titled “1. Install packages”](#1-install-packages)
```bash
pip install mercury pandas skrub
```
## 2. Import libraries
[Section titled “2. Import libraries”](#2-import-libraries)
```python
import mercury as mr
from IPython.display import clear_output
```
Note
`clear_output` from IPython is used to hide empty cells when no file has been uploaded yet, keeping the app clean on first load.
## 3. Add the file upload widget
[Section titled “3. Add the file upload widget”](#3-add-the-file-upload-widget)
```python
input_file = mr.UploadFile(label="Upload your Dataset", accept='.csv', max_file_size='1GB')
```
[`UploadFile`](/docs/widgets/uploadfile/) renders a file picker in the app sidebar. We restrict it to `.csv` files and allow up to 1 GB.

When the user picks a file:
* `input_file.name` becomes the filename (non-empty string),
* `input_file.value` contains the raw file bytes.
Mercury automatically re-runs the notebook when the upload changes, so all cells below react immediately.
## 4. Read the CSV
[Section titled “4. Read the CSV”](#4-read-the-csv)
```python
if input_file.name is not None:
import pandas as pd
from io import BytesIO
from skrub import TableReport
data = BytesIO(input_file.value)
df = pd.read_csv(data)
```
We wrap the raw bytes in `BytesIO` so pandas can read them directly, without saving the file to disk first.
## 5. Compute the summary statistics
[Section titled “5. Compute the summary statistics”](#5-compute-the-summary-statistics)
```python
if input_file.name is not None:
# shape
row_count = df.shape[0]
col_count = df.shape[1]
# missing values
missing_count = df.isna().sum().sum()
missing_procent = df.isna().mean().mean() * 100
# duplicates
duplicates_count = df.duplicated().sum()
duplicates_procent = (duplicates_count / row_count) * 100
```
Straightforward pandas — nothing unusual here. We keep both the raw counts and percentages so we can show both in the indicators.
## 6. Detect column types
[Section titled “6. Detect column types”](#6-detect-column-types)
This is the most interesting part of the app. We go beyond pandas’ built-in dtypes to catch edge cases.
```python
# basic pandas types
numeric_columns = df.select_dtypes(include=["number"]).columns.tolist()
boolean_columns = df.select_dtypes(include=["bool"]).columns.tolist()
datetime_columns = df.select_dtypes(include=["datetime", "datetimetz"]).columns.tolist()
object_columns = df.select_dtypes(include=["object", "category"]).columns.tolist()
```
### Detecting dates stored as text
[Section titled “Detecting dates stored as text”](#detecting-dates-stored-as-text)
A very common real-world problem: date columns that look like `"2024-01-15"` but are stored as plain strings. Pandas reads them as `object` dtype and misses them. We catch them manually:
```python
detected_datetime_columns = []
for col in object_columns:
converted = pd.to_datetime(df[col], errors="coerce")
valid_ratio = converted.notna().mean()
if valid_ratio > 0.8:
detected_datetime_columns.append(col)
datetime_columns = list(set(datetime_columns + detected_datetime_columns))
```
If more than 80% of values in a column parse as a valid date, we treat it as datetime.
### Detecting free-text columns
[Section titled “Detecting free-text columns”](#detecting-free-text-columns)
Long string columns (descriptions, comments, addresses) shouldn’t be counted as categorical. We separate them out by average string length:
```python
text_columns = []
for col in object_columns:
if col not in datetime_columns:
avg_text_length = df[col].dropna().astype(str).str.len().mean()
if avg_text_length > 30:
text_columns.append(col)
```
### Constant columns
[Section titled “Constant columns”](#constant-columns)
Columns with only one unique value carry no information and are worth flagging:
```python
constant_columns = [
col for col in all_columns
if df[col].nunique(dropna=False) <= 1
]
```
### Categorical columns
[Section titled “Categorical columns”](#categorical-columns)
Everything that doesn’t fall into numeric, boolean, datetime, or text:
```python
excluded_from_categorical = set(
datetime_columns + text_columns + boolean_columns + numeric_columns
)
categorical_columns = [
col for col in all_columns
if col not in excluded_from_categorical
]
```
## 7. Build the output widgets
[Section titled “7. Build the output widgets”](#7-build-the-output-widgets)
### Indicators
[Section titled “Indicators”](#indicators)
```python
ind_basic = mr.Indicator([
mr.Indicator(value=row_count, label="Rows"),
mr.Indicator(value=col_count, label="Columns"),
mr.Indicator(value=duplicates_count, label="Duplicate Rows", delta=f"{duplicates_procent:.2f}%"),
mr.Indicator(value=missing_count, label="Missing Values", delta=f"{missing_procent:.2f}%"),
])
```
[`Indicator`](/docs/widgets/indicator/) displays a big number with an optional delta label below it. Nesting multiple `Indicator` objects inside one renders them side by side.

### Column type table
[Section titled “Column type table”](#column-type-table)
```python
data = {
"Metric": ["Numeric Columns", "Categorical Columns", "Text Columns",
"Datetime Columns", "Boolean Columns", "Constant Columns"],
"Value": [len(numeric_columns), len(categorical_columns), len(text_columns),
len(datetime_columns), len(boolean_columns), len(constant_columns)],
"Description": [
"Columns with numeric values",
"Columns with categorical values",
"Columns with longer text values",
"Columns detected as dates or timestamps",
"Columns with true/false values",
"Columns with only one unique value",
]
}
col_type_table = mr.Table(data)
```
[`Table`](/docs/widgets/table/) renders a plain dict or DataFrame as a clean HTML table.

### Data preview
[Section titled “Data preview”](#data-preview)
```python
report = TableReport(df, n_rows=15)
display(report)
```
`TableReport` from [skrub](https://skrub-data.org/) renders an interactive table with per-column statistics, sortable headers, and value distributions. It does a lot of work for one line of code.

## 8. Conditional display
[Section titled “8. Conditional display”](#8-conditional-display)
The last cells switch between the welcome state and the populated state, depending on whether a file has been uploaded:
```python
# welcome message
if input_file.name is None:
_ = mr.Markdown("# Upload the dataset and check the summary")
else:
_ = mr.Markdown("# Dataset summary", key="title")
```
`mr.Markdown(...)` renders immediately, so no `display()` wrapper is needed. Assigning its returned widget to `_` prevents IPython from displaying the last expression a second time.
The same conditional pattern repeats for the indicators, the column type table, and the data preview:
```python
if input_file.name is not None:
display(ind_basic)
else:
clear_output(wait=False)
```
`clear_output()` hides the cell output entirely when there’s nothing to show, so the app doesn’t leave empty gaps on first load. `display(ind_basic)` is needed here because `ind_basic` was created earlier in the computation cell. Mercury widgets created inline display immediately; assign their return values to avoid IPython displaying them again.
## 9. Run as a web app
[Section titled “9. Run as a web app”](#9-run-as-a-web-app)
Start the Mercury server from the folder containing the notebook:
```bash
mercury
```
Mercury will detect all `*.ipynb` files and serve them as web applications.
## Notes and tips
[Section titled “Notes and tips”](#notes-and-tips)
* The 80% threshold for datetime detection works well in practice, but you can tune it for stricter or looser detection.
* The text column threshold of 30 characters is a heuristic. Short categoricals like country names average well under 30; free-text fields like comments average well above.
* `TableReport` can be slow on very large files. Consider adding `df = df.sample(10_000)` before calling it if you expect datasets with hundreds of thousands of rows.
* To support Excel files as well, add `accept='.csv,.xlsx'` to `UploadFile` and handle both formats with `pd.read_csv` / `pd.read_excel` based on `input_file.name`.
# Tutorials
> Step-by-step guides for bulding web apps, dashboards, chat bots with Mercury.
A collection of tutorials to help you get started:
[Create Your First Dashboard ](/tutorials/dashboard/sales-dashboard)Step-by-step tutorial for building sales dashboard, with filters, KPIs and chart.
# Create Your First Dashboard in Python (Step-by-Step Tutorial)
> In this tutorial, you will learn how to create your first interactive dashboard using a Python notebook and the Mercury framework. We will build a simple sales dashboard with filters, metrics, and charts.
Dashboards are one of the most common use cases for **Mercury web apps**.\
They allow you to combine **filters, metrics, and charts** into a single interactive view - all created directly from a Python notebook.
In this tutorial, you will build your **first sales dashboard** step by step. We will use **Mercury** widgets and **Altair** package for visualizations.
Note
This example uses a synthetic dataset to focus on dashboard structure and concepts.
## App preview
[Section titled “App preview”](#app-preview)

You can try the live dashboard:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/sales-dashboard.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/sales-dashboard?no-navbar)
## 1. Import Mercury and needed packages
[Section titled “1. Import Mercury and needed packages”](#1-import-mercury-and-needed-packages)
Start by importing Mercury and needed packages in the first notebook cell:
```python
import numpy as np
import pandas as pd
import altair as alt
import mercury as mr
```
We will use the `mr` alias to create all dashboard widgets.
The `pandas` and `numpy` packages are used for data generation and manipulation. The `altair` package is for visualization.
## 2. Prepare example data
[Section titled “2. Prepare example data”](#2-prepare-example-data)
In the next cell, prepare a simple sales dataset using `pandas`.
```python
# Example sales DataFrame
rng = np.random.default_rng(7)
dates = pd.date_range("2025-01-01", "2025-06-30", freq="D")
regions = ["North", "South", "West", "East"]
channels = ["Online", "Retail", "Partners"]
products = ["Starter", "Pro", "Enterprise"]
n = 2500 # number of rows
df = pd.DataFrame(
{
"date": rng.choice(dates, size=n),
"region": rng.choice(regions, size=n, p=[0.28, 0.22, 0.25, 0.25]),
"channel": rng.choice(channels, size=n, p=[0.55, 0.35, 0.10]),
"product": rng.choice(products, size=n, p=[0.55, 0.35, 0.10]),
"units": rng.integers(1, 9, size=n),
}
)
# Simple pricing model
price_map = {"Starter": 49, "Pro": 129, "Enterprise": 399}
df["unit_price"] = df["product"].map(price_map).astype(float)
df["revenue"] = (df["units"] * df["unit_price"]).round(2)
```
This dataset represents sales revenue across multiple regions for three different plans.
## 3. Create dashboard filters
[Section titled “3. Create dashboard filters”](#3-create-dashboard-filters)
Dashboards usually start with **filters**. Here, we let the user select region, channel, product, metric and granularity.
```python
region_sel = mr.MultiSelect(label="Region", choices=regions, value=regions)
channel_sel = mr.MultiSelect(label="Channel", choices=channels, value=channels)
product_sel = mr.MultiSelect(label="Product", choices=products, value=products)
metric = mr.Select(label="Chart metric", choices=["Revenue", "Units"], value="Revenue")
granularity = mr.Select(label="Time granularity", choices=["Day", "Week", "Month"], value="Week")
```
The [`MultiSelect`](docs/input/multiselect/) and [`Select`](docs/input/select/) widgets allows users to control what data is shown in the dashboard.
## 4. Filter the data
[Section titled “4. Filter the data”](#4-filter-the-data)
Now filter the dataset based on values in widgets:
```python
# filter data
mask = (
(df["region"].isin(region_sel.value))
& (df["channel"].isin(channel_sel.value))
& (df["product"].isin(product_sel.value))
)
dff = df.loc[mask].copy()
```
This filtered DataFrame will be reused across **metrics and charts**, keeping the dashboard consistent.
## 5. Display key metrics
[Section titled “5. Display key metrics”](#5-display-key-metrics)
Dashboards often show **high-level numbers** at the top.
```python
total_revenue = float(dff["revenue"].sum())
total_units = int(dff["units"].sum())
orders = int(len(dff))
aov = (total_revenue / orders) if orders else 0.0
mr.Indicator(
[
mr.Indicator(value=f"{total_revenue:,.0f}", label="Total revenue"),
mr.Indicator(value=f"{total_units:,.0f}", label="Units sold"),
mr.Indicator(value=round(aov, 2), label="Avg order value"),
]
)
```
## 6. Create a chart
[Section titled “6. Create a chart”](#6-create-a-chart)
Next, visualize the data using **Altair**.
```python
dff["date"] = pd.to_datetime(dff["date"])
if granularity.value == "Day":
dff["period"] = dff["date"].dt.date.astype("datetime64[ns]")
elif granularity.value == "Week":
dff["period"] = dff["date"].dt.to_period("W").dt.start_time
else: # Month
dff["period"] = dff["date"].dt.to_period("M").dt.start_time
agg = (
dff.groupby(["period", "region"], as_index=False)
.agg(revenue=("revenue", "sum"), units=("units", "sum"))
.sort_values("period")
)
y_field = "revenue" if metric.value == "Revenue" else "units"
y_title = "Revenue" if metric.value == "Revenue" else "Units"
chart = (
alt.Chart(agg)
.mark_line(point=True)
.encode(
x=alt.X("period:T", title=""),
y=alt.Y(f"{y_field}:Q", title=y_title),
color=alt.Color("region:N", title="Region"),
tooltip=[
alt.Tooltip("period:T", title="Period"),
alt.Tooltip("region:N", title="Region"),
alt.Tooltip("revenue:Q", title="Revenue", format=",.2f"),
alt.Tooltip("units:Q", title="Units", format=",.0f"),
],
)
.properties(height=420, width=550)
.interactive()
)
chart
```
Mercury automatically renders the chart as part of the dashboard output.
## 7. Why this works
[Section titled “7. Why this works”](#7-why-this-works)
* Mercury turns **notebook cells into UI components**
* Widgets (`MultiSelect` and `Select`) control the data flow
* Metrics and charts update automatically
* No frontend code is required
* The notebook can be deployed as a **web dashboard**
## Next steps
[Section titled “Next steps”](#next-steps)
Once you are comfortable with the basics, you can:
* Add date pickers and sliders
* Use multiple charts
* Load data from CSV or databases
* Add authentication
* Deploy dashboards for your team
👍 You have just built your **first interactive dashboard** using Python and Mercury.
# Create Your First Sales Report in Python with Mercury
> Build a Markdown-first sales report in Python with Mercury. Add inline filters, compute KPIs, and render an Altair time-series chart directly from a notebook.
This page documents the **exact notebook code** from `sales-report.ipynb`.
The notebook is a **report-style app** (Markdown-first), with:
* **inline filters** (Region + Time granularity)
* computed **KPIs**
* an **Altair** line chart (Revenue over time)
* a short **Conclusions** section
Note
The dataset is synthetic (generated with NumPy) so you can focus on the report structure.
## App preview
[Section titled “App preview”](#app-preview)

Try the live report:
🚀 Load interactive demo Hover to start
[🛠️ Source code ](https://github.com/mljar/mercury/blob/main/docs/notebooks/sales-report.ipynb)[⛅ Open demo in new tab](https://docs.ismvp.org/mercury/sales-report?no-navbar)
## 1. Imports
[Section titled “1. Imports”](#1-imports)
Your first cell imports exactly these packages:
```python
import numpy as np
import pandas as pd
import altair as alt
import mercury as mr
```
* `mr` is used for widgets and rich Markdown sections
* `altair` is used for the interactive chart
## 2. Report introduction (Markdown cell)
[Section titled “2. Report introduction (Markdown cell)”](#2-report-introduction-markdown-cell)
Your notebook starts with a Markdown section that introduces the report:
```md
## Sales in Q1, Q2 2025 📊
...
```
This is the key difference between **reports** and dashboards: the report is meant to be read top-to-bottom, like a document.
***
## 3. Generate example sales data
[Section titled “3. Generate example sales data”](#3-generate-example-sales-data)
Your dataset is generated with NumPy and stored in a pandas DataFrame.
```python
# Example sales DataFrame
rng = np.random.default_rng(7)
dates = pd.date_range("2025-01-01", "2025-06-30", freq="D")
regions = ["North", "South", "West", "East"]
channels = ["Online", "Retail", "Partners"]
products = ["Starter", "Pro", "Enterprise"]
n = 2500 # number of rows
df = pd.DataFrame(
{
"date": rng.choice(dates, size=n),
"region": rng.choice(regions, size=n, p=[0.28, 0.22, 0.25, 0.25]),
"channel": rng.choice(channels, size=n, p=[0.55, 0.35, 0.10]),
"product": rng.choice(products, size=n, p=[0.55, 0.35, 0.10]),
"units": rng.integers(1, 9, size=n),
}
)
# Simple pricing model
price_map = {"Starter": 49, "Pro": 129, "Enterprise": 399}
df["unit_price"] = df["product"].map(price_map).astype(float)
df["revenue"] = (df["units"] * df["unit_price"]).round(2)
```
## 4. Inline filters (Region + Granularity)
[Section titled “4. Inline filters (Region + Granularity)”](#4-inline-filters-region--granularity)
In your notebook the widgets are created **inline** using `position="inline"`:
```python
region_sel = mr.MultiSelect(label="Region", choices=regions, value=regions, position="inline")
granularity = mr.Select(label="Time granularity", choices=["Day", "Week", "Month"], value="Week", position="inline")
```
These widgets control:
* which regions are included
* how time is aggregated in the chart
Related docs:
* [`MultiSelect`](/docs/input/multiselect/)
* [`Select`](/docs/input/select/)
## 5. Filter the data
[Section titled “5. Filter the data”](#5-filter-the-data)
Your filtering logic is intentionally minimal — the report filters only by region:
```python
# filter data
mask = (df["region"].isin(region_sel.value))
dff = df.loc[mask].copy()
```
## 6. Compute KPIs
[Section titled “6. Compute KPIs”](#6-compute-kpis)
Next, the notebook calculates the core report metrics:
```python
total_revenue = float(dff["revenue"].sum())
total_units = int(dff["units"].sum())
orders = int(len(dff))
aov = (total_revenue / orders) if orders else 0.0
```
## 7. Dynamic report section with `mr.Markdown(f"...")`
[Section titled “7. Dynamic report section with mr.Markdown(f"...")”](#7-dynamic-report-section-with-mrmarkdownf)
This is the “report” part: you render a summary as Markdown, using live values from widgets and KPIs.
```python
_ = mr.Markdown(
f"""
### Report scope
- **Regions:** {region_sel.value}
- **Granularity:** {granularity.value}
### Key results
- **Total revenue:** {total_revenue:,.0f}
- **Units sold:** {total_units:,.0f}
- **Orders:** {orders:,.0f}
- **Average order value (AOV):** {aov:,.2f}
"""
)
```
Assigning the returned widget to `_` prevents IPython from rendering the final cell expression a second time. Mercury already displays `Markdown()` immediately.
Why it’s useful:
* you can keep the UI minimal
* the output reads like a human report
* numbers update automatically when filters change
## 8. Chart description (Markdown cell)
[Section titled “8. Chart description (Markdown cell)”](#8-chart-description-markdown-cell)
Before the chart, your notebook includes this Markdown explanation:
```md
The chart below aggregates the data by the selected time granularity and splits lines by region.
```
This keeps the report readable.
## 9. Build the Altair chart (time aggregation + revenue lines)
[Section titled “9. Build the Altair chart (time aggregation + revenue lines)”](#9-build-the-altair-chart-time-aggregation--revenue-lines)
Your chart code:
* derives `period` based on the granularity widget
* aggregates revenue/units
* plots **revenue** (fixed in this notebook)
```python
dff["date"] = pd.to_datetime(dff["date"])
if granularity.value == "Day":
dff["period"] = dff["date"].dt.date.astype("datetime64[ns]")
elif granularity.value == "Week":
dff["period"] = dff["date"].dt.to_period("W").dt.start_time
else: # Month
dff["period"] = dff["date"].dt.to_period("M").dt.start_time
agg = (
dff.groupby(["period", "region"], as_index=False)
.agg(revenue=("revenue", "sum"), units=("units", "sum"))
.sort_values("period")
)
y_field = "revenue"
y_title = "Revenue"
chart = (
alt.Chart(agg)
.mark_line(point=True)
.encode(
x=alt.X("period:T", title=""),
y=alt.Y(f"{y_field}:Q", title=y_title),
color=alt.Color("region:N", title="Region"),
tooltip=[
alt.Tooltip("period:T", title="Period"),
alt.Tooltip("region:N", title="Region"),
alt.Tooltip("revenue:Q", title="Revenue", format=",.2f"),
alt.Tooltip("units:Q", title="Units", format=",.0f"),
],
)
.properties(height=320, width=700)
.interactive()
)
```
## 10. Render the chart
[Section titled “10. Render the chart”](#10-render-the-chart)
Your notebook renders the chart by returning the variable:
```python
chart
```
Mercury automatically displays it in the report flow.
## 11. Conclusions (Markdown cell)
[Section titled “11. Conclusions (Markdown cell)”](#11-conclusions-markdown-cell)
Your report ends with a conclusions section:
```md
## Conclusions
Sales are very good
```
This is where you can add interpretation, notes, and next steps.
# How to Use url_key in Mercury Widgets
> Learn how to use url_key in Mercury widgets, including supported widgets, validation rules, examples, and shared url_key behavior.
The `url_key` argument lets you initialize widget values directly from URL query parameters.
This is useful when you want to:
* prefill widgets from a shared link,
* create bookmarkable app states,
* link dashboards with predefined filters,
* pass parameters from another app into a Mercury notebook.
## What `url_key` does
[Section titled “What url\_key does”](#what-url_key-does)
When a widget supports `url_key`, Mercury reads the corresponding query parameter from the page URL during initialization and uses it to override the widget’s default `value`.
In practice, this means that code like this:
```python
import mercury as mr
country = mr.Select(
label="Country",
choices=["Poland", "Germany", "France"],
value="Poland",
url_key="country"
)
```
can be initialized from a URL such as:
```text
http://localhost:8888/mercury/example_notebook?country=Germany
```
In that case, the widget starts with `"Germany"` instead of `"Poland"`.
Note
`url_key` affects the initial widget value during app startup. It does not continuously sync the widget with later URL changes.
## Widgets that support `url_key`
[Section titled “Widgets that support url\_key”](#widgets-that-support-url_key)
The following Mercury widgets support `url_key`:
* `TextInput`
* `NumberInput`
* `Slider`
* `Select`
* `MultiSelect`
* `CheckBox`
## Validation rules by widget
[Section titled “Validation rules by widget”](#validation-rules-by-widget)
Each widget validates URL values differently before accepting them.
TextInput
* reads the first value for the given `url_key`
* accepts non-empty text values
* uses the widget `value` if the URL value is invalid
NumberInput
* reads the first value for the given `url_key`
* accepts numeric values
* clamps values to the valid range and nearest step
* uses the widget `value` if the URL value is invalid
Slider
* reads the first value for the given `url_key`
* accepts numeric values
* clamps values to the valid range
* uses the widget `value` if the URL value is invalid
Select
* reads the first value for the given `url_key`
* accepts non-empty values that match one of `choices`
* uses the widget `value` if the URL value is invalid
MultiSelect
* reads all values for the given `url_key`
* accepts non-empty values that match one of `choices`
* uses the widget `value` if the URL value is invalid
CheckBox
* reads the first value for the given `url_key`
* accepts only `true` or `false`
* uses the widget `value` if the URL value is invalid
## Examples
[Section titled “Examples”](#examples)
### TextInput
[Section titled “TextInput”](#textinput)
**Code**
```python
import mercury as mr
username = mr.TextInput(
label="User name",
value="guest",
url_key="username"
)
username.value
```
**Example URL**
```text
http://localhost:8888/mercury/example_notebook?username=jan
```
**Result**

***
### NumberInput
[Section titled “NumberInput”](#numberinput)
**Code**
```python
import mercury as mr
rows = mr.NumberInput(
label="Rows",
value=10,
min=0,
max=100,
step=5,
url_key="rows"
)
rows.value
```
**Example URL**
```text
http://localhost:8888/mercury/example_notebook?rows=18
```
**Result**

The value is **20** because with `step=5`, 20 is the closest valid value to 18.
***
### Slider
[Section titled “Slider”](#slider)
**Code**
```python
import mercury as mr
age = mr.Slider(
label="Age",
value=25,
min=0,
max=100,
url_key="age"
)
age.value
```
**Example URL**
```text
http://localhost:8888/mercury/example_notebook?age=42
```
**Result**

***
### Select
[Section titled “Select”](#select)
**Code**
```python
import mercury as mr
country = mr.Select(
label="Country",
choices=["Poland", "Germany", "France"],
value="Poland",
url_key="country"
)
country.value
```
**Example URL**
```text
http://localhost:8888/mercury/example_notebook?country=Germany
```
**Result**

***
### MultiSelect
[Section titled “MultiSelect”](#multiselect)
**Code**
```python
import mercury as mr
fruits = mr.MultiSelect(
label="Fruits",
choices=["Apple", "Banana", "Orange", "Kiwi"],
value=["Apple"],
url_key="fruit"
)
fruits.value
```
**Example URL**
```text
http://localhost:8888/mercury/example_notebook?fruit=Banana&fruit=Orange
```
**Result**
The widget starts with:

***
### CheckBox
[Section titled “CheckBox”](#checkbox)
**Code**
```python
import mercury as mr
show_details = mr.CheckBox(
label="Show details",
value=False,
url_key="details"
)
show_details.value
```
**Example URL**
```text
http://localhost:8888/mercury/example_notebook?details=true
```
**Result**

Tip
**Use descriptive keys** that explain what the value represents.
* Good examples: `country`, `rows`, `show_details`, `fruit`
* Avoid generic names like `x`, `v`, or `param1`.
## What happens if multiple widgets use the same `url_key`?
[Section titled “What happens if multiple widgets use the same url\_key?”](#what-happens-if-multiple-widgets-use-the-same-url_key)
Multiple widgets can use the same `url_key`, but each widget interprets the URL value using its own validation rules.
That means the same URL parameter may be accepted by some widgets and ignored by others.
### Example
[Section titled “Example”](#example)
**Code**
```python
import mercury as mr
text_username = mr.TextInput(
label="Text username",
value="guest",
url_key="user"
)
select_username = mr.Select(
label="Select username",
choices=["guest", "admin", "editor"],
value="guest",
url_key="user"
)
number_username = mr.NumberInput(
label="Numeric user id",
value=1,
min=1,
max=100,
url_key="user"
)
```
**Example URL**
```text
http://localhost:8888/mercury/example_notebook?user=admin
```
**Result**

**What happens**
* `TextInput` becomes `"admin"`
* `Select` becomes `"admin"` because it is present in `choices`
* `NumberInput` ignores `"admin"` (nonnumeric value) and keeps its fallback `value`
Note
Sharing the same `url_key` between multiple widgets is supported, but it is usually clearer to use separate keys unless you intentionally want several widgets to derive state from the same URL parameter.
## Summary
[Section titled “Summary”](#summary)
`url_key` makes it possible to initialize Mercury widgets from URL query parameters, enabling shareable links, bookmarkable state, and URL-driven defaults.
Choose a clear `url_key`, provide a sensible fallback `value`, and test the exact URL formats your app should support.