Streamlit custom component for rendering and editing schema graphs
Project description
streamlit-schema-editor
streamlit-schema-editor is a Streamlit custom component for schema
visualization, editing, ER-style canvases, and advanced lineage or mapping
workflows built on React Flow and Streamlit Custom Components v2.
| Light mode | Dark mode |
|---|---|
Features
- Schema viewer and editable ER diagram canvas in one component
- Table, column, relationship, and optional group-based lane model
- Inline column create, update, and delete interactions
- Relationship create/delete flows with duplicate and per-handle limit checks
- Optional mid-edge action button for relationship inspection
- Validation-aware styling for tables, columns, and relationships
- Automatic group layout in
manual,columns, orrowsmode - Structured
eventandevent_contextpayloads for Streamlit workflows - Metadata passthrough on groups, tables, columns, and relationships
Demo App
The deployable demo in demo/ is a getting-started sandbox for trying the
component inside a regular Streamlit app. It starts with a relationship-free
schema viewer preset, then lets you switch to an ER diagram or a grouped
source-to-target example for richer mapping workflows.
demo/requirements.txtpins the publishedstreamlit-schema-editorpackage used by the demo on Streamlit Community Cloud.- The demo imports that installed package instead of the repository checkout, so deployments do not need to build frontend assets on the Cloud machine.
- Switch between schema viewer, editable ER diagram, and mapping canvases
- Filter or reshape tables and relationships before passing them into
streamlit_schema_editor(...) - Test editing, selection, validation styling, layout options, and edge actions
- Compare the default relationship-free viewer with ER and mapping workflows
How it Works
The canvas layer is built on React Flow from
xyflow, using the
@xyflow/react library under the
hood. streamlit-schema-editor wraps those primitives in a Streamlit-friendly
API while still exposing familiar capabilities from the React Flow ecosystem.
Capability in streamlit-schema-editor |
React Flow primitive | Docs |
|---|---|---|
| Pan/zoom canvas, selection, and viewport management | ReactFlow |
API reference |
| Zoom controls and fit-view button | Controls |
Component docs |
| Canvas dot grid | Background |
Component docs |
| Column connection handles | Handle |
Component docs |
| Relationship paths, markers, and custom edge actions | BaseEdge and getBezierPath |
BaseEdge, getBezierPath |
Install
With uv:
uv add streamlit-schema-editor
With pip:
pip install streamlit-schema-editor
Then open the demo app and use the controls to:
- Create, inspect, and delete relationships between tables
- Drag tables around the canvas and preserve updated positions
- Toggle controls, grouping, validation display, and editability
- Inspect the returned
eventandevent_contextvalues in Streamlit
Example
import streamlit as st
from streamlit_schema_editor import streamlit_schema_editor
if "tables" not in st.session_state:
st.session_state.tables = [
{
"id": "customers",
"label": "customers",
"metadata": {"schema": "sales"},
"columns": [
{"id": "id", "name": "id", "data_type": "uuid"},
{"id": "email", "name": "email", "data_type": "varchar"},
{"id": "created_at", "name": "created_at", "data_type": "timestamp"},
],
},
{
"id": "orders",
"label": "orders",
"metadata": {"schema": "sales"},
"columns": [
{"id": "id", "name": "id", "data_type": "uuid"},
{"id": "customer_id", "name": "customer_id", "data_type": "uuid"},
{"id": "status", "name": "status", "data_type": "varchar"},
{"id": "ordered_at", "name": "ordered_at", "data_type": "timestamp"},
],
},
{
"id": "order_items",
"label": "order_items",
"metadata": {"schema": "sales"},
"columns": [
{"id": "id", "name": "id", "data_type": "uuid"},
{"id": "order_id", "name": "order_id", "data_type": "uuid"},
{"id": "product_id", "name": "product_id", "data_type": "uuid"},
{"id": "quantity", "name": "quantity", "data_type": "int4"},
],
},
{
"id": "products",
"label": "products",
"metadata": {"schema": "catalog"},
"columns": [
{"id": "id", "name": "id", "data_type": "uuid"},
{"id": "sku", "name": "sku", "data_type": "varchar"},
{"id": "price", "name": "price", "data_type": "money"},
],
},
]
if "relationships" not in st.session_state:
st.session_state.relationships = [
{
# Relationship ids only need to be unique and stable. A structured
# id like this makes generated events easy to trace back to columns.
"id": "rel::orders::customer_id::customers::id",
"source_table": "orders",
"source_column": "customer_id",
"target_table": "customers",
"target_column": "id",
"label": "belongs to",
"validation": {
"status": "success",
"summary": "Foreign key exists in the warehouse.",
},
"metadata": {"constraint_name": "fk_orders_customer_id"},
},
{
"id": "rel::order_items::order_id::orders::id",
"source_table": "order_items",
"source_column": "order_id",
"target_table": "orders",
"target_column": "id",
"label": "part of",
"metadata": {"constraint_name": "fk_order_items_order_id"},
},
{
"id": "rel::order_items::product_id::products::id",
"source_table": "order_items",
"source_column": "product_id",
"target_table": "products",
"target_column": "id",
"label": "references",
"metadata": {"constraint_name": "fk_order_items_product_id"},
},
]
value = streamlit_schema_editor(
st.session_state.tables,
st.session_state.relationships,
height=700,
show_controls=True,
show_arrowheads=False,
max_connections_per_handle=2,
key="schema-editor",
)
st.session_state.tables = value["tables"]
st.session_state.relationships = value["relationships"]
st.write(value["event"])
st.json(value["event_context"])
More Examples
Interactive editor with inline type suggestions and relationship inspection:
value = streamlit_schema_editor(
tables,
relationships,
editable=True,
show_edge_button=True,
show_column_count_badge=True,
column_type_options=["string", "bigint", "timestamp", "json"],
key="schema-editor-with-edge-actions",
)
if value["event"] == "edge_details_requested":
relationship_id = (value["event_context"] or {}).get("relationship_id")
st.write(f"Inspect relationship: {relationship_id}")
Viewer-style canvas with grouping and auto layout:
value = streamlit_schema_editor(
tables,
relationships,
groups=[
{"id": "source", "label": "Source"},
{"id": "target", "label": "Target"},
],
editable=False,
connectable=False,
deletable=False,
draggable=True,
show_groups=True,
group_layout="columns",
group_order=["source", "target"],
table_layout_within_group="stack",
show_controls=True,
key="schema-viewer",
)
Hide validation visuals until the user opts in:
show_validation = st.toggle("Show validation", value=True)
value = streamlit_schema_editor(
tables,
relationships,
show_validation=show_validation,
key="schema-editor-validation-toggle",
)
Force validation visuals to refresh on demand:
if "validation_refresh_nonce" not in st.session_state:
st.session_state.validation_refresh_nonce = 0
if st.button("Refresh validation visuals"):
st.session_state.validation_refresh_nonce += 1
value = streamlit_schema_editor(
tables,
relationships,
show_validation=True,
validation_refresh_key=st.session_state.validation_refresh_nonce,
key="schema-editor-validation-refresh",
)
Advanced source-to-target mapping with shared mapping_group_id metadata for
multi-branch targets:
value = streamlit_schema_editor(
groups=[
{"id": "source", "label": "Source"},
{"id": "target", "label": "Target"},
],
tables=[
{
"id": "raw.customer_profile",
"label": "raw.customer_profile",
"group_id": "source",
"columns": [
{"id": "customer_id", "name": "customer_id", "data_type": "string"},
{"id": "first_name", "name": "first_name", "data_type": "string"},
{"id": "last_name", "name": "last_name", "data_type": "string"},
],
},
{
"id": "curated.customer_dim",
"label": "curated.customer_dim",
"group_id": "target",
"columns": [
{"id": "customer_id", "name": "customer_id", "data_type": "string"},
{
"id": "full_name",
"name": "full_name",
"data_type": "string",
"metadata": {
"mapping_group_id": "customer_full_name",
"expression": "concat_ws(' ', first_name, last_name)",
},
},
],
},
],
relationships=[
{
"id": "rel::raw.customer_profile::first_name::curated.customer_dim::full_name",
"source_table": "raw.customer_profile",
"source_column": "first_name",
"target_table": "curated.customer_dim",
"target_column": "full_name",
"label": "concat",
"metadata": {
"mapping_group_id": "customer_full_name",
"branch_order": 1,
"expression": "concat_ws(' ', first_name, last_name)",
},
},
{
"id": "rel::raw.customer_profile::last_name::curated.customer_dim::full_name",
"source_table": "raw.customer_profile",
"source_column": "last_name",
"target_table": "curated.customer_dim",
"target_column": "full_name",
"label": "concat",
"metadata": {
"mapping_group_id": "customer_full_name",
"branch_order": 2,
"expression": "concat_ws(' ', first_name, last_name)",
},
},
],
show_groups=True,
group_layout="columns",
table_layout_within_group="stack",
show_edge_button=True,
key="source-target-mapping",
)
The component treats metadata as application-owned payload, so you can attach
domain concepts like mapping_group_id, SQL expressions, generated-target
markers, lineage ids, or transformation notes without the wrapper needing to
know their schema.
Run the bundled examples from the project root:
uv run streamlit run examples/playground.py
uv run streamlit run examples/schema_viewer.py
uv run streamlit run examples/er_diagram.py
uv run streamlit run examples/databricks_mapping.py
demo/streamlit_app.py: deployable demo app for Streamlit Community Cloud. It usesdemo/requirements.txtto install the published package.examples/playground.py: interactive playground for runtime options, grouping visibility, and validationexamples/schema_viewer.py: read-only schema browserexamples/er_diagram.py: ER-style relationship view with arrowheads hiddenexamples/databricks_mapping.py: Databricks-inspired source-to-target mapping demo with labeled group lanes
API
streamlit_schema_editor(...) accepts a domain-shaped API and returns the latest
graph state plus a semantic event payload for the current rerun.
Component props
| Prop | Type | Default | Description |
|---|---|---|---|
tables |
list[TableSpec] |
required | Schema tables to render. |
relationships |
list[RelationshipSpec] |
required | Relationships between columns. |
groups |
list[GroupSpec] | None |
None |
Optional group or lane containers. |
height |
int |
600 |
Canvas height in pixels. |
fit_view |
bool |
True |
Auto-fit the visible graph on first render. |
editable |
bool |
True |
Enable inline column editing and add/delete actions. |
connectable |
bool | None |
editable |
Enable column handles and relationship creation. |
draggable |
bool | None |
True |
Allow tables to be repositioned. |
deletable |
bool | None |
editable |
Allow delete-key removal for nodes and edges. |
show_controls |
bool |
False |
Show React Flow zoom and fit controls. |
show_arrowheads |
bool |
True |
Draw arrow markers on relationships. |
show_edge_button |
bool |
False |
Show the inline relationship action button. |
show_column_count_badge |
bool |
True |
Show a column-count badge in each table header. |
show_groups |
bool |
True |
Render group or lane panels when group data is present. |
group_layout |
Literal["manual", "columns", "rows"] |
"manual" |
Group placement strategy. |
group_order |
list[str] | None |
None |
Stable display ordering for groups. |
table_layout_within_group |
Literal["manual", "stack"] |
"manual" |
Table placement strategy inside each group. |
show_validation |
bool |
True |
Enable validation badges and status coloring. |
validation_refresh_key |
str | int | float | None |
None |
Force validation UI refresh after upstream state changes. |
column_type_options |
list[str] | None |
None |
Suggestions for the inline data type picker. |
allow_zoom |
bool |
True |
Enable wheel, pinch, and double-click zoom. |
allow_duplicate_edges |
bool |
False |
Allow multiple identical relationships. |
max_connections_per_handle |
int | None |
None |
Apply the same incoming and outgoing connection limit to each handle. |
max_incoming_connections_per_handle |
int | None |
None |
Cap incoming relationships per handle. |
max_outgoing_connections_per_handle |
int | None |
None |
Cap outgoing relationships per handle. |
max_incoming_per_target |
int | None |
None |
Legacy alias for max_incoming_connections_per_handle. |
max_outgoing_per_source |
int | None |
None |
Legacy alias for max_outgoing_connections_per_handle. |
key |
str | None |
None |
Optional Streamlit component key. |
editable acts as the default for connectable and deletable. Dragging is
enabled by default even for read-only canvases, and editable also enables
inline column editing, row add controls, keyboard deletion, and column edit events. Override
the interaction flags individually when you need a mixed mode.
Use show_arrowheads=False for ER-style views where you want the canvas to read
more like an undirected diagram, and show_column_count_badge=False when long
table names need the extra header space. The legacy aliases
max_incoming_per_target and max_outgoing_per_source are still accepted for
backward compatibility, but the generic connection-limit parameters are the
preferred public API.
Return value
| Key | Type | Description |
|---|---|---|
groups |
list[GroupSpec] |
Current group list with updated positions and sizes. |
tables |
list[TableSpec] |
Current table list with updated positions and inline edits. |
relationships |
list[RelationshipSpec] |
Current relationships after connect/delete actions. |
selection |
SelectionState |
Current selected table, column, and relationship ids. |
event |
SchemaEditorEvent |
Semantic event name or None. |
event_context |
dict | None |
Structured payload for the last event. |
Key Classes
| Type | Required fields | Optional fields | Notes |
|---|---|---|---|
GroupSpec |
id, label |
position, width, height, metadata |
Use for group or lane containers. |
TableSpec |
id, label, columns |
position, group_id, validation, metadata |
group_id ties a table to a GroupSpec. |
ColumnSpec |
id, name, data_type |
validation, metadata |
metadata is passed through untouched. |
RelationshipSpec |
id, source_table, source_column, target_table, target_column |
label, validation, metadata |
id can be any unique, stable string. Structured ids are useful for deterministic event handling, debugging, and duplicate detection. |
ValidationSpec |
none | status, code, summary, detail |
status supports initial, success, loading, and error. |
Position |
x, y |
none | Used by groups, tables, and move events. |
Use validation for generic UI state that the component understands, and keep
app-specific semantics in metadata. For example, SQL expressions, lineage
attributes, ownership, or workflow IDs should live in metadata, not as
first-class component fields.
Events
Events are dispatched in response to actions. You can use these to define your own event handlers. The full list of supported events are described in this table:
| Event | Description |
|---|---|
selection_changed |
Fires when table, column, or relationship selection changes. |
node_moved |
Fires when a table position changes. |
table_deleted |
Fires after tables are removed, including any deleted relationship ids. |
column_created |
Fires after a column is added. |
column_updated |
Fires after an inline column edit is saved. |
column_deleted |
Fires after a column is removed, including any deleted relationship ids. |
relationship_created |
Fires after a new relationship is created. |
relationship_deleted |
Fires with the deleted relationship id. |
relationship_rejected |
Fires when duplicate or handle-limit rules reject a relationship. |
edge_details_requested |
Fires when the relationship action button is clicked. |
history_undone |
Fires after the built-in undo control restores the previous graph state. |
history_redone |
Fires after the built-in redo control reapplies a graph state. |
edge_details_requested is only emitted when show_edge_button=True.
Handle inline column edit events in Streamlit:
value = streamlit_schema_editor(
tables,
relationships,
editable=True,
key="schema-editor-events",
)
if value["event"] == "column_created":
st.success(f"Added column: {(value['event_context'] or {}).get('column_id')}")
if value["event"] == "column_updated":
context = value["event_context"] or {}
st.info(
"Updated "
f"{context.get('table_id')}.{context.get('column_id')} "
f"fields={context.get('fields')}"
)
if value["event"] == "column_deleted":
context = value["event_context"] or {}
st.warning(
"Deleted "
f"{context.get('table_id')}.{context.get('column_id')} "
f"and removed relationships={context.get('deleted_relationship_ids')}"
)
Usage Notes
- Inline editing, hover state, and connection gestures stay local to the React component until they emit durable graph state back to Streamlit.
- Arbitrary metadata is passed through untouched on groups, tables, columns, and relationships.
- The component is designed for rerun-driven usage: persist returned
tablesandrelationshipsinst.session_statewhen edits should survive subsequent Streamlit reruns. - Use
groupsplus per-tablegroup_idfor optional labeled containers such asSource/Target, database lanes, or bronze/silver/gold layers. - Automatic layout modes own placement, so
group_layout="columns"orgroup_layout="rows"pair best with read-only or lightly interactive canvases. - Column ids remain stable and are not edited inline. If you delete a column, any attached relationships are removed from the graph at the same time.
Repository Structure
This repository is structured like a standard Streamlit custom component project:
streamlit_schema_editor/contains the publishable Python packagestreamlit_schema_editor/frontend/contains the React Flow frontend componentdemo/contains the Streamlit Community Cloud demo appexamples/contains smaller Streamlit examples for manual testingtests/contains Python-side and component regression coveragepyproject.tomlbuilds the package asstreamlit-schema-editor
Local Validation
From the repository root:
uv sync --extra dev
cd streamlit_schema_editor/frontend
npm install
npm run build
cd ../..
uv build
uv run pytest
To run the primary demo app:
python -m pip install -r demo/requirements.txt
streamlit run demo/streamlit_app.py
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file streamlit_schema_editor-0.2.0.tar.gz.
File metadata
- Download URL: streamlit_schema_editor-0.2.0.tar.gz
- Upload date:
- Size: 232.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f8772da4a534d49f62f0ff4eb3629df94c1d3a7ce919381fc5687628daae8187
|
|
| MD5 |
4642f8a85f6668331c42db91178402f7
|
|
| BLAKE2b-256 |
fffe559df20454205ac8cb6b97501a90b10657bc88dfd48e7053faa2e91e679a
|
File details
Details for the file streamlit_schema_editor-0.2.0-py3-none-any.whl.
File metadata
- Download URL: streamlit_schema_editor-0.2.0-py3-none-any.whl
- Upload date:
- Size: 170.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
205551f1a4692d6ce24dc0be6c835bc63340d3a3899d1ad18fa4c34b0f63da63
|
|
| MD5 |
8cefbb1a80683e5d78af5d5062a3bdf9
|
|
| BLAKE2b-256 |
8609de25c7ee3880a9857074512a63943443e0c6f526e8e8b92fbf4838eed1e5
|