diff --git a/README.md b/README.md
index 208ea8c1..5f088756 100644
--- a/README.md
+++ b/README.md
@@ -10,74 +10,102 @@ A Streamlit Component is made out of a Python API and a frontend (built using an
A Component can be used in any Streamlit app, can pass data between Python and frontend code, and can optionally be distributed on [PyPI](https://pypi.org/) for the rest of the world to use.
-- Create a component's API in a single line of Python:
-
- ```python
- import streamlit.components.v1 as components
-
- # Declare the component:
- my_component = components.declare_component("my_component", path="frontend/build")
-
- # Use it:
- my_component(greeting="Hello", name="World")
- ```
-
-- Build the component's frontend out of HTML and JavaScript (or TypeScript, or ClojureScript, or whatever you fancy). React is supported, but not required:
-
- ```tsx
- import React from 'react';
- import {
- withStreamlitConnection,
- ComponentProps,
- } from 'streamlit-component-lib';
-
- function MyComponent({ args }: ComponentProps) {
- // Access arguments from Python via `props.args`:
- const { greeting, name } = args;
- return (
-
- {greeting}, {name}!
-
- );
- }
-
- export default withStreamlitConnection(MyComponent);
- ```
-
-## Quickstart
-
-- Ensure you have [Python 3.9+](https://www.python.org/downloads/), [Node.js](https://nodejs.org), and [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed.
-- Clone this repo.
-- Create a new Python virtual environment for the template:
- ```bash
- $ cd template
- $ python3 -m venv venv # create venv
- $ . venv/bin/activate # activate venv
- $ pip install streamlit # install streamlit
- ```
-- Initialize and run the component template frontend:
- ```bash
- $ cd template/my_component/frontend
- $ npm install # Install npm dependencies
- $ npm run start # Start the Vite dev server
- ```
-- From a separate terminal, run the template's Streamlit app:
- ```bash
- $ cd template
- $ . venv/bin/activate # activate the venv you created earlier
- $ pip install -e . # install template as editable package
- $ streamlit run my_component/example.py # run the example
- ```
-- If all goes well, you should see something like this:
- 
-- Modify the frontend code at `my_component/frontend/src/MyComponent.tsx`.
-- Modify the Python code at `my_component/__init__.py`.
+```python
+import streamlit as st
-## Examples
+# Declare the component
+my_component = st.components.v2.component(
+ "your-package.your_component",
+ js="index-*.js",
+ html='',
+)
+
+# Use it directly or via a small wrapper
+value = my_component(data={"name": "World"}, default={"num_clicks": 0})
+```
+
+```tsx
+import { Component } from '@streamlit/component-v2-lib';
+import { createRoot } from 'react-dom/client';
+
+const MyComponent: Component = (args) => {
+ const root = createRoot(args.parentElement.querySelector('.react-root'));
+ root.render(Hello, {args.data.name}!
);
+};
+
+export default MyComponent;
+```
+
+See full examples in `templates/v2/template/` and `templates/v2/template-reactless/`.
+
+
+Show v1 code samples
+
+```python
+import streamlit.components.v1 as components
+
+# Declare the component (v1)
+my_component = components.declare_component("my_component", path="frontend/build")
+
+# Use it
+value = my_component(name="World", default=0)
+```
+
+```tsx
+import {
+ withStreamlitConnection,
+ ComponentProps,
+} from 'streamlit-component-lib';
+
+function MyComponent({ args }: ComponentProps) {
+ return Hello, {args.name}!\n
;
+}
+
+export default withStreamlitConnection(MyComponent);
+```
+
+
+
+See full examples in `templates/v1/template/` and `templates/v1/template-reactless/`.
-See the `template-reactless` directory for a template that does not use [React](https://reactjs.org/).
+## Supported template versions
+
+This repo provides templates for both Streamlit Component APIs:
+
+- v2: Uses `st.components.v2.component()` and `@streamlit/component-v2-lib`.
+- v1: Uses `st.components.v1.component()` and `streamlit-component-lib`.
+
+## Quickstart (generate a new component with Cookiecutter)
+
+- Ensure you have [uv](https://docs.astral.sh/uv/), [Node.js](https://nodejs.org), and [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed.
+- Generate from this template using Cookiecutter via `uvx`:
+
+ - v2 (recommended):
+
+ ```bash
+ uvx --from cookiecutter cookiecutter gh:streamlit/component-template --directory cookiecutter/v2
+ ```
+
+ - v1:
+ ```bash
+ uvx --from cookiecutter cookiecutter gh:streamlit/component-template --directory cookiecutter/v1
+ ```
+
+- Follow the interactive prompts to generate your project.
+- Once created, follow the `README` in your new project to get started.
+
+## Just browsing? Pre-generated outputs
+
+If you want to quickly explore without running Cookiecutter, browse the pre-generated templates in this repo:
+
+- v2 outputs: `templates/v2/template/` and `templates/v2/template-reactless/`
+- v1 outputs: `templates/v1/template/` and `templates/v1/template-reactless/`
+
+From within one of these folders, you can inspect the Python package layout and the `my_component/frontend` code. Refer to the template-level `README` for build instructions.
+
+## Examples
-See the `examples` directory for examples on working with pandas DataFrames, integrating with third-party libraries, and more.
+See the `examples/v1/` directory for examples working with pandas DataFrames, integrating with third-party libraries, and more.
## Community-provided Templates
diff --git a/cookiecutter/v2/{{ cookiecutter.package_name }}/README.md b/cookiecutter/v2/{{ cookiecutter.package_name }}/README.md
index bcdf2856..76d37c3d 100644
--- a/cookiecutter/v2/{{ cookiecutter.package_name }}/README.md
+++ b/cookiecutter/v2/{{ cookiecutter.package_name }}/README.md
@@ -8,6 +8,14 @@
uv pip install {{ cookiecutter.package_name }}
```
+### Development install (editable)
+
+When developing this component locally, install it in editable mode so Streamlit picks up code changes without rebuilding a wheel. Run this from the directory that contains `pyproject.toml`:
+
+```sh
+uv pip install -e . --force-reinstall
+```
+
## Usage instructions
```python
diff --git a/cookiecutter/v2/{{ cookiecutter.package_name }}/e2e/test_template.py b/cookiecutter/v2/{{ cookiecutter.package_name }}/e2e/test_template.py
deleted file mode 100644
index a3fdaf8d..00000000
--- a/cookiecutter/v2/{{ cookiecutter.package_name }}/e2e/test_template.py
+++ /dev/null
@@ -1,84 +0,0 @@
-from pathlib import Path
-
-import pytest
-from e2e_utils import StreamlitRunner
-from playwright.sync_api import Page, expect
-
-ROOT_DIRECTORY = Path(__file__).parent.parent.absolute()
-BASIC_EXAMPLE_FILE = ROOT_DIRECTORY / "my_component" / "example.py"
-
-
-@pytest.fixture(autouse=True, scope="module")
-def streamlit_app():
- with StreamlitRunner(BASIC_EXAMPLE_FILE) as runner:
- yield runner
-
-
-@pytest.fixture(autouse=True, scope="function")
-def go_to_app(page: Page, streamlit_app: StreamlitRunner):
- page.goto(streamlit_app.server_url)
- # Wait for app to load
- page.get_by_role("img", name="Running...").is_hidden()
-
-
-def test_should_render_template(page: Page):
- frame_0 = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(0)
- frame_1 = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(1)
-
- st_markdown_0 = page.get_by_role("paragraph").nth(0)
- st_markdown_1 = page.get_by_role("paragraph").nth(1)
-
- expect(st_markdown_0).to_contain_text("You've clicked 0 times!")
-
- frame_0.get_by_role("button", name="Click me!").click()
-
- expect(st_markdown_0).to_contain_text("You've clicked 1 times!")
- expect(st_markdown_1).to_contain_text("You've clicked 0 times!")
-
- frame_1.get_by_role("button", name="Click me!").click()
- frame_1.get_by_role("button", name="Click me!").click()
-
- expect(st_markdown_0).to_contain_text("You've clicked 1 times!")
- expect(st_markdown_1).to_contain_text("You've clicked 2 times!")
-
- page.get_by_label("Enter a name").click()
- page.get_by_label("Enter a name").fill("World")
- page.get_by_label("Enter a name").press("Enter")
-
- expect(frame_1.get_by_text("Hello, World!")).to_be_visible()
-
- frame_1.get_by_role("button", name="Click me!").click()
-
- expect(st_markdown_0).to_contain_text("You've clicked 1 times!")
- expect(st_markdown_1).to_contain_text("You've clicked 3 times!")
-
-
-def test_should_change_iframe_height(page: Page):
- frame = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(1)
-
- expect(frame.get_by_text("Hello, Streamlit!")).to_be_visible()
-
- locator = page.locator('iframe[title="my_component\\.my_component"]').nth(1)
-
- page.wait_for_timeout(1000)
- init_frame_height = locator.bounding_box()["height"]
- assert init_frame_height != 0
-
- page.get_by_label("Enter a name").click()
-
- page.get_by_label("Enter a name").fill(35 * "Streamlit ")
- page.get_by_label("Enter a name").press("Enter")
-
- expect(frame.get_by_text("Streamlit Streamlit Streamlit")).to_be_visible()
-
- page.wait_for_timeout(1000)
- frame_height = locator.bounding_box()["height"]
- assert frame_height > init_frame_height
-
- page.set_viewport_size({"width": 150, "height": 150})
-
- expect(frame.get_by_text("Streamlit Streamlit Streamlit")).not_to_be_in_viewport()
-
- page.wait_for_timeout(1000)
- frame_height_after_viewport_change = locator.bounding_box()["height"]
- assert frame_height_after_viewport_change > frame_height
diff --git a/quickstart.png b/quickstart.png
deleted file mode 100644
index be8f9aa3..00000000
Binary files a/quickstart.png and /dev/null differ
diff --git a/templates/v2/template-reactless/README.md b/templates/v2/template-reactless/README.md
index 94e710e5..f60ab3dd 100644
--- a/templates/v2/template-reactless/README.md
+++ b/templates/v2/template-reactless/README.md
@@ -8,6 +8,14 @@ Streamlit component that allows you to do X
uv pip install streamlit-custom-component
```
+### Development install (editable)
+
+When developing this component locally, install it in editable mode so Streamlit picks up code changes without rebuilding a wheel. Run this from the directory that contains `pyproject.toml`:
+
+```sh
+uv pip install -e . --force-reinstall
+```
+
## Usage instructions
```python
diff --git a/templates/v2/template-reactless/e2e/test_template.py b/templates/v2/template-reactless/e2e/test_template.py
deleted file mode 100644
index a3fdaf8d..00000000
--- a/templates/v2/template-reactless/e2e/test_template.py
+++ /dev/null
@@ -1,84 +0,0 @@
-from pathlib import Path
-
-import pytest
-from e2e_utils import StreamlitRunner
-from playwright.sync_api import Page, expect
-
-ROOT_DIRECTORY = Path(__file__).parent.parent.absolute()
-BASIC_EXAMPLE_FILE = ROOT_DIRECTORY / "my_component" / "example.py"
-
-
-@pytest.fixture(autouse=True, scope="module")
-def streamlit_app():
- with StreamlitRunner(BASIC_EXAMPLE_FILE) as runner:
- yield runner
-
-
-@pytest.fixture(autouse=True, scope="function")
-def go_to_app(page: Page, streamlit_app: StreamlitRunner):
- page.goto(streamlit_app.server_url)
- # Wait for app to load
- page.get_by_role("img", name="Running...").is_hidden()
-
-
-def test_should_render_template(page: Page):
- frame_0 = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(0)
- frame_1 = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(1)
-
- st_markdown_0 = page.get_by_role("paragraph").nth(0)
- st_markdown_1 = page.get_by_role("paragraph").nth(1)
-
- expect(st_markdown_0).to_contain_text("You've clicked 0 times!")
-
- frame_0.get_by_role("button", name="Click me!").click()
-
- expect(st_markdown_0).to_contain_text("You've clicked 1 times!")
- expect(st_markdown_1).to_contain_text("You've clicked 0 times!")
-
- frame_1.get_by_role("button", name="Click me!").click()
- frame_1.get_by_role("button", name="Click me!").click()
-
- expect(st_markdown_0).to_contain_text("You've clicked 1 times!")
- expect(st_markdown_1).to_contain_text("You've clicked 2 times!")
-
- page.get_by_label("Enter a name").click()
- page.get_by_label("Enter a name").fill("World")
- page.get_by_label("Enter a name").press("Enter")
-
- expect(frame_1.get_by_text("Hello, World!")).to_be_visible()
-
- frame_1.get_by_role("button", name="Click me!").click()
-
- expect(st_markdown_0).to_contain_text("You've clicked 1 times!")
- expect(st_markdown_1).to_contain_text("You've clicked 3 times!")
-
-
-def test_should_change_iframe_height(page: Page):
- frame = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(1)
-
- expect(frame.get_by_text("Hello, Streamlit!")).to_be_visible()
-
- locator = page.locator('iframe[title="my_component\\.my_component"]').nth(1)
-
- page.wait_for_timeout(1000)
- init_frame_height = locator.bounding_box()["height"]
- assert init_frame_height != 0
-
- page.get_by_label("Enter a name").click()
-
- page.get_by_label("Enter a name").fill(35 * "Streamlit ")
- page.get_by_label("Enter a name").press("Enter")
-
- expect(frame.get_by_text("Streamlit Streamlit Streamlit")).to_be_visible()
-
- page.wait_for_timeout(1000)
- frame_height = locator.bounding_box()["height"]
- assert frame_height > init_frame_height
-
- page.set_viewport_size({"width": 150, "height": 150})
-
- expect(frame.get_by_text("Streamlit Streamlit Streamlit")).not_to_be_in_viewport()
-
- page.wait_for_timeout(1000)
- frame_height_after_viewport_change = locator.bounding_box()["height"]
- assert frame_height_after_viewport_change > frame_height
diff --git a/templates/v2/template/README.md b/templates/v2/template/README.md
index 94e710e5..f60ab3dd 100644
--- a/templates/v2/template/README.md
+++ b/templates/v2/template/README.md
@@ -8,6 +8,14 @@ Streamlit component that allows you to do X
uv pip install streamlit-custom-component
```
+### Development install (editable)
+
+When developing this component locally, install it in editable mode so Streamlit picks up code changes without rebuilding a wheel. Run this from the directory that contains `pyproject.toml`:
+
+```sh
+uv pip install -e . --force-reinstall
+```
+
## Usage instructions
```python
diff --git a/templates/v2/template/e2e/test_template.py b/templates/v2/template/e2e/test_template.py
deleted file mode 100644
index a3fdaf8d..00000000
--- a/templates/v2/template/e2e/test_template.py
+++ /dev/null
@@ -1,84 +0,0 @@
-from pathlib import Path
-
-import pytest
-from e2e_utils import StreamlitRunner
-from playwright.sync_api import Page, expect
-
-ROOT_DIRECTORY = Path(__file__).parent.parent.absolute()
-BASIC_EXAMPLE_FILE = ROOT_DIRECTORY / "my_component" / "example.py"
-
-
-@pytest.fixture(autouse=True, scope="module")
-def streamlit_app():
- with StreamlitRunner(BASIC_EXAMPLE_FILE) as runner:
- yield runner
-
-
-@pytest.fixture(autouse=True, scope="function")
-def go_to_app(page: Page, streamlit_app: StreamlitRunner):
- page.goto(streamlit_app.server_url)
- # Wait for app to load
- page.get_by_role("img", name="Running...").is_hidden()
-
-
-def test_should_render_template(page: Page):
- frame_0 = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(0)
- frame_1 = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(1)
-
- st_markdown_0 = page.get_by_role("paragraph").nth(0)
- st_markdown_1 = page.get_by_role("paragraph").nth(1)
-
- expect(st_markdown_0).to_contain_text("You've clicked 0 times!")
-
- frame_0.get_by_role("button", name="Click me!").click()
-
- expect(st_markdown_0).to_contain_text("You've clicked 1 times!")
- expect(st_markdown_1).to_contain_text("You've clicked 0 times!")
-
- frame_1.get_by_role("button", name="Click me!").click()
- frame_1.get_by_role("button", name="Click me!").click()
-
- expect(st_markdown_0).to_contain_text("You've clicked 1 times!")
- expect(st_markdown_1).to_contain_text("You've clicked 2 times!")
-
- page.get_by_label("Enter a name").click()
- page.get_by_label("Enter a name").fill("World")
- page.get_by_label("Enter a name").press("Enter")
-
- expect(frame_1.get_by_text("Hello, World!")).to_be_visible()
-
- frame_1.get_by_role("button", name="Click me!").click()
-
- expect(st_markdown_0).to_contain_text("You've clicked 1 times!")
- expect(st_markdown_1).to_contain_text("You've clicked 3 times!")
-
-
-def test_should_change_iframe_height(page: Page):
- frame = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(1)
-
- expect(frame.get_by_text("Hello, Streamlit!")).to_be_visible()
-
- locator = page.locator('iframe[title="my_component\\.my_component"]').nth(1)
-
- page.wait_for_timeout(1000)
- init_frame_height = locator.bounding_box()["height"]
- assert init_frame_height != 0
-
- page.get_by_label("Enter a name").click()
-
- page.get_by_label("Enter a name").fill(35 * "Streamlit ")
- page.get_by_label("Enter a name").press("Enter")
-
- expect(frame.get_by_text("Streamlit Streamlit Streamlit")).to_be_visible()
-
- page.wait_for_timeout(1000)
- frame_height = locator.bounding_box()["height"]
- assert frame_height > init_frame_height
-
- page.set_viewport_size({"width": 150, "height": 150})
-
- expect(frame.get_by_text("Streamlit Streamlit Streamlit")).not_to_be_in_viewport()
-
- page.wait_for_timeout(1000)
- frame_height_after_viewport_change = locator.bounding_box()["height"]
- assert frame_height_after_viewport_change > frame_height