# streamlit-drawable-canvas puts a Fabric.js sketchpad inside a Streamlit app

> streamlit-drawable-canvas is a Streamlit custom component that renders a Fabric.js canvas for freehand drawing, shapes, text and labelled boxes, and returns the drawn objects as JSON and the canvas as image data. It is a best effort project with a MIT licence, and its own README asks that a native implementation be built into Streamlit instead.

**andfanilo/streamlit-drawable-canvas** — Do you like Quick, Draw? Well what if you could train/predict doodles drawn inside Streamlit? Also draws lines, circles and boxes over background images for annotation.

- Repository: https://github.com/andfanilo/streamlit-drawable-canvas
- Website: https://drawable-canvas.streamlit.app/
- Stars: 670 · Forks: 120
- Language: Python
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/andfanilo-streamlit-drawable-canvas

## What the component actually does inside a Streamlit page

streamlit-drawable-canvas is a Streamlit custom component providing a sketching canvas built on Fabric.js. The component is invoked as a function, st_canvas, which returns a result object carrying the canvas image and the underlying drawing data.

The feature list covers a usable annotation tool rather than a toy. You can draw freely, and you can add lines, circles, boxes and polygons, with options for stroke and fill. Any object on the canvas can be rotated, skewed, scaled and moved on demand. A background color or image can be chosen to draw on. The data comes back to Streamlit, both the image data and the properties of every drawn object, and you choose whether that data is fetched back in realtime or only when a button is pressed. Undo, redo and delete of canvas contents are supported, and the canvas data can be saved as JSON so it can be reused in another session.

The project description gives the motivation in an unusual way. It asks whether you have seen the Quick, Draw game and then asks what if you could train and predict on doodles drawn inside Streamlit, and it also frames the component as a way to draw lines, circles and boxes over background images for annotation. So the intended use is a machine learning annotation surface, where a person sketches a digit or boxes an object and the resulting JSON becomes training data.

The repository shows how that is built. There is a streamlit_drawable_canvas package directory with a frontend subdirectory, an e2e_playwright directory for end to end tests, a demo_app.py, a tests directory, and a FAQ.md. The frontend is a separate TypeScript application built with Vite, and the justfile shows the split, with a setup-frontend recipe running npm ci in the frontend directory and a build-frontend step whose output is served by Streamlit.

## Installing the component and the Streamlit version it demands

Installation is one command, but the version requirement is the part to read carefully.

```bash
pip install streamlit-drawable-canvas
```

The README states that the component requires Streamlit 1.53 or newer and Python 3.10 or newer. The pyproject.toml confirms both, with requires-python of 3.10 or later and a single runtime dependency, streamlit >= 1.53, and classifiers for Python 3.10 through 3.13.

The 1.53 floor is a consequence of the component protocol change. The README notes that 0.10.0 is built on Streamlit Components v2, and that anyone on an older Streamlit should read the section on upgrading from 0.9.x. In practice this means an app pinned to an older Streamlit cannot run the current component without a Streamlit upgrade first, and that upgrade is not a small change for a large app.

There is a detail in the packaging worth knowing. The pyproject.toml declares an optional dependency group called image, and it is intentionally empty, with a comment pointing at the CHANGELOG. The pyproject.toml only notes that the group is intentionally empty and points at the CHANGELOG, so if your app uses uploaded background images you need to install Pillow yourself, as the example imports it but the component does not pull it in.

For local development the justfile provides the whole workflow. The setup recipe runs uv sync for the Python side, npm ci in the frontend directory, and installs the pre-commit hooks, while the dev recipe starts a Vite watch build alongside a running Streamlit process that picks up the rebuilt frontend on refresh. The app-run recipe serves demo_app.py, and there are separate recipes for linting and formatting the Python and TypeScript sides.

## Wiring st_canvas into a page and reading the result

The example in the README is a complete page, and it shows the pattern most applications will follow: the drawing configuration lives in the sidebar, the canvas is rendered by one call, and the result is then used.

```python
import pandas as pd
from PIL import Image
import streamlit as st
from streamlit_drawable_canvas import st_canvas
```

The component call itself carries the configuration:

```python
canvas_result = st_canvas(
    fill_color="rgba(255, 165, 0, 0.3)",  # Fixed fill color with some opacity
    stroke_width=stroke_width,
    stroke_color=stroke_color,
    background_color=bg_color,
    background_image=Image.open(bg_image) if bg_image else None,
    update_streamlit=realtime_update,
    height=150,
    drawing_mode=drawing_mode,
    point_display_radius=point_display_radius if drawing_mode == "point" else 0,
    return_image_data=True,
    key="canvas",
)
```

Then the returned data is consumed. The image data goes straight to st.image, and the JSON data is normalised into a dataframe with pandas:

```python
if canvas_result.json_data is not None:
    objects = pd.json_normalize(
        canvas_result.json_data["objects"]
    )  # need to convert obj to str because PyArrow
    for col in objects.select_dtypes(include=["object"]).columns:
        objects[col] = objects[col].astype("str")
    st.dataframe(objects)
```

The comment about converting objects to strings is a real practical detail rather than decoration. The object properties include values that Arrow cannot type directly, so the dataframe is built and then every object column is cast to a string before it is displayed.

The README also points to FAQ.md for the questions that come up after the first working version: what is in json_data, why a resized shape keeps its original width, and how to map canvas coordinates back to the source image. That last one matters for anyone annotating an image, because the canvas coordinates are canvas coordinates and not image coordinates.

## The parameters, and the ones with behaviour you cannot guess

The API is a single function with a fixed list of keyword arguments:

```python
st_canvas(
    fill_color: str
    stroke_width: int
    stroke_color: str
    background_color: str
    background_image: str | Path | bytes | Image
    update_streamlit: bool
    height: int
    width: int
    drawing_mode: str
    initial_drawing: dict
    point_display_radius: int
    return_image_data: bool
    key: str
    on_change: callable
    disabled: bool
    background_image_fit: str
    max_display_height: int
    font_size: int
    label: str
)
```

Several defaults and side effects are documented, and they are the ones that will surprise you.

fill_color defaults to #eee, but in drawing_mode of text it defaults to stroke_color instead, on the reasoning that #eee text on a default canvas would be nearly invisible. background_color defaults to an empty string, which is transparent, and it is overridden by background_image. Changing either background_color or background_image resets the drawing, so a background that depends on a session value will wipe the user's work on every change.

background_image accepts an http(s) URL, a data URI, a local file path, raw bytes or a Pillow Image, and it is resized automatically to the canvas dimensions. Being behind the canvas, it is not sent back to Streamlit on a mouse event, so a large background does not travel on every interaction.

drawing_mode accepts freedraw, line, rect, circle, point, polygon, text and labeled_rect, and defaults to freedraw. Any other value raises a ValueError. The polygon mode is interactive in a specific way: you click to add a vertex, each vertex shows a handle, clicking the first vertex's handle closes the shape, and clicking any other handle removes that vertex. The text mode places an empty text object and starts editing immediately, and nothing is sent to Streamlit until editing ends. labeled_rect draws a box the same way rect does but stamps it with the current label value, which is how you build a labelling tool in a few lines.

## Realtime updates, and why the README steers you to st.form

update_streamlit controls when data flows back to the server, and the documentation is unusually opinionated about it.

When it is true, canvas data is sent to Streamlit whenever an object or selection is updated or the mouse is released. When it is false, the toolbar stays pinned open rather than appearing on hover, because in that mode the toolbar's send button is the only discoverable way to commit a drawing. That is a small UI decision that tells you the author has thought about discoverability rather than just toggling a flag.

There is one forced exception. update_streamlit is turned off automatically for polygon mode, on the grounds that an in-progress multi-click polygon is not a meaningful intermediate value, while the completed polygon still sends once it is closed.

And then the recommendation, which is the most useful sentence in the parameter documentation. If what you want is only the finished drawing, prefer an st.form over update_streamlit=False. The reason is that a form gives you a submit action that commits the drawing, whereas disabling realtime updates leaves the user with a button whose behaviour differs from the rest of the app's submission model. For an annotation app where a drawing is the expensive artefact and you want to review it before recording it, that is the pattern to follow.

The API also carries initial_drawing for restoring a saved session, on_change for a callback, disabled for locking the canvas, max_display_height and font_size, and background_image_fit for how the background is scaled.

## Maintenance reality and what the maintainer says about it

The opening paragraph of the README is the part any adopter should read first, because it sets expectations clearly. The project is described as best effort. The maintainer says that every now and then they will add something they need themselves and let a coding agent do most of the typing, but that they do not have the time to go through bigger issues or pull requests, and that anyone wanting a larger feature should fork away.

That is a candid statement of a single-maintainer project with irregular capacity, and it is worth taking at face value. It also points to a specific ask: the README asks readers to add a thumbs up on a Streamlit issue requesting a native implementation maintained by the Streamlit team, because the project would be better as a first-party component.

The release history is consistent with a project that ships in batches. The recent tags are v0.13.0 with labelled boxes, v0.12.0 with the text tool, point editing and the edit toggle, and v0.11.0 with a toolbar cleanup and lock-aware transform mode, all dated 2026-09-04. The last push was on 2026-09-04 as well, and the repository is not archived, with the default branch being develop.

The licence is MIT, and the pyproject.toml records it along with a license-files entry pointing at the LICENSE file, so the terms are as permissive as they are simple. The project also carries a CHANGELOG, a CONTRIBUTING file, an AGENTS.md and a CLAUDE.md, which tells you the project has been maintained with the help of coding agents and now documents that practice for contributors.

## Conclusion

Adopt streamlit-drawable-canvas if you are building a Streamlit app that needs a sketchpad, an image annotation surface or a doodle classifier training input, and you can live with a component whose own README describes it as best effort and asks for a native equivalent. Do not adopt it for a mission critical annotation tool, because the maintainer states there is no time for larger issues or pull requests, and do not expect a smooth update path, since the component requires Streamlit 1.53 or newer and 0.10.0 was rebuilt on Components v2. Verify first that your Streamlit version satisfies the 1.53 floor and that you can accept the Python 3.10 minimum, that the object data you need survives the json_data round trip, reading FAQ.md for what the returned structure contains, and that the drawing_mode value you want exists, since an unsupported mode raises ValueError rather than degrading.

## FAQ

### How do I install streamlit-drawable-canvas and what are the requirements?

Run pip install streamlit-drawable-canvas. The README states that it requires Streamlit 1.53 or newer and Python 3.10 or newer, and the pyproject.toml records a single runtime dependency of streamlit >= 1.53 with a requires-python of 3.10 or later.

### How do I get the drawn objects back from st_canvas?

The component returns a result object with json_data and image_data. The README example normalises the objects with pd.json_normalize on canvas_result.json_data["objects"] and casts the object columns to strings before showing them in st.dataframe, because PyArrow cannot type those values directly.

### How do I draw a labelled bounding box in streamlit-drawable-canvas?

Set drawing_mode to labeled_rect, which draws a box the same way rect does but stamps it with the current label value. The label parameter sets that value, and the text mode behaves differently, placing an empty text object that starts editing immediately and is not sent to Streamlit until editing ends.

### Should I use update_streamlit False to get the finished drawing only?

The README says to prefer an st.form over update_streamlit=False, because a form gives you a submit action that commits the drawing. When update_streamlit is false the toolbar stays pinned open because its send button is then the only discoverable way to commit a drawing, and the flag is forced off for polygon mode.

### Is streamlit-drawable-canvas actively maintained?

The README describes the project as best effort, says the maintainer does not have the time for bigger issues or pull requests, and asks readers who want a larger feature to fork. The recent tags are v0.13.0, v0.12.0 and v0.11.0, and the last push was on 2026-09-04.

## Sources

- [andfanilo/streamlit-drawable-canvas on GitHub](https://github.com/andfanilo/streamlit-drawable-canvas)
- [License: MIT](https://github.com/andfanilo/streamlit-drawable-canvas/blob/develop/LICENSE)
- [Project website](https://drawable-canvas.streamlit.app/)
- [README](https://github.com/andfanilo/streamlit-drawable-canvas/blob/develop/README.md)
- [Releases](https://github.com/andfanilo/streamlit-drawable-canvas/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/andfanilo-streamlit-drawable-canvas
