Use traditional HTML/CSS/JS file structure with Streamlit while keeping the benefits of free Streamlit deployment.
- 🔥 Hot Reload - Component files auto-reload during development
- 🔄 State Management - Real-time state sync with conflict resolution
- 📋 Event Replay - Record and replay component events
- ✅ Props Validation - JSON Schema validation for component props
- 📦 Component Registry - Early validation at startup
- 📁 Organized File Structure - Separate HTML/CSS/JS files (traditional web development workflow)
- 🔄 Template Variables - Jinja2-powered props and data binding
- 🎨 Framework Integration - Easy setup with Tailwind, Bootstrap, Bulma, and more
- ⚡ Performance Caching - Multi-level caching for fast rendering
- 🚀 Streamlit Cloud Ready - Works with free Streamlit deployment
- 🛡️ Security Built-in - XSS prevention and input validation
- 🔥 Hot Reload - Component files auto-reload during development (no restart needed!)
- 🔌 Enhanced Bidirectional Communication - State management, event replay, conflict resolution
- ✅ Props Validation - JSON Schema or manual validation rules
- 📦 Component Registry - Validate components at startup, not render time
- 🔍 Fuzzy Matching - Smart error messages with suggestions
- 📊 State History - Track and rollback state changes
- 🎯 Event Recording - Automatic event history with replay capability
pip install streamlit-html-componentsOr install from source:
git clone https://github.com/cjcarito/streamlit-html-components
cd streamlit-html-components
pip install -e .my_app/
├── app.py
└── components/
├── templates/
│ └── button.html
├── styles/
│ └── button.css
└── scripts/
└── button.js
templates/button.html:
<button class="custom-btn" id="myBtn">
{{ text }}
</button>styles/button.css:
.custom-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px 32px;
border: none;
border-radius: 8px;
cursor: pointer;
}scripts/button.js:
document.getElementById('myBtn').addEventListener('click', function() {
alert('Button clicked!');
});app.py:
import streamlit as st
from streamlit_html_components import render_component, configure
# Configure component directories
configure(
templates_dir='components/templates',
styles_dir='components/styles',
scripts_dir='components/scripts'
)
st.title("My Custom Button")
# Render your component
render_component(
'button', # Component name (matches template filename)
props={'text': 'Click me!'}, # Variables passed to template
height=100
)streamlit run app.pyThat's it! Your custom HTML/CSS/JS component is now running in Streamlit.
The v2 API provides enhanced features with better validation, immutable configuration, and improved performance.
- ✅ Component Registry - Validate components at startup, not render time
- 🔒 Immutable Configuration - Pydantic-based type-safe configuration
- 🚀 Better Caching - Cache keys based on actual file content
- 📝 Better Error Messages - Detailed validation errors with suggestions
- 🔍 Auto-discovery - Automatically find and register components
import streamlit as st
from streamlit_html_components import configure_v2, render_component_v2
# Configure with auto-discovery
configure_v2(
templates_dir='components/templates',
styles_dir='components/styles',
scripts_dir='components/scripts',
frameworks=['tailwind'],
auto_discover=True # Automatically register all components
)
st.title("My App with v2 API")
# Render component (validated at startup)
render_component_v2('button', props={'text': 'Click me!'})Modern configuration with Pydantic validation:
from streamlit_html_components import configure_v2
configure_v2(
templates_dir='components/templates',
styles_dir='components/styles',
scripts_dir='components/scripts',
frameworks=['tailwind', 'bootstrap'],
# Cache settings
enable_cache=True,
cache_max_size_mb=100,
cache_ttl_seconds=300,
# Security settings
enable_csp=True,
allowed_origins=['*'],
validate_paths=True,
# Auto-discovery
auto_discover=True # Scan and register all components
)Render components with registry validation:
from streamlit_html_components import render_component_v2
render_component_v2(
component_name='button',
props={'text': 'Click me'},
height=100,
cache=True,
on_event=callback_function
)Manually register components:
from streamlit_html_components import register_component
register_component(
name='custom_button',
template='button.html',
styles=['button.css', 'animations.css'],
scripts=['button.js'],
validate=True # Validate files exist at registration time
)List all registered components:
from streamlit_html_components import list_components
components = list_components()
print(components) # ['button', 'card', 'hero', ...]Get component metadata:
from streamlit_html_components import get_component_info
info = get_component_info('button')
print(info.template) # 'button.html'
print(info.styles) # ['button.css']
print(info.scripts) # ['button.js']The v2 API is backward compatible. You can use both APIs in the same app:
from streamlit_html_components import (
configure, render_component, # v1 API
configure_v2, render_component_v2 # v2 API
)
# Use v1 for backward compatibility
configure(templates_dir='old_components/templates')
render_component('legacy_button', props={'text': 'Old API'})
# Use v2 for new components
configure_v2(templates_dir='new_components/templates', auto_discover=True)
render_component_v2('modern_button', props={'text': 'New API'})Why migrate to v2?
- Earlier error detection - Components validated at startup
- Better caching - Invalidates cache when files change
- Type safety - Pydantic validates all configuration
- Immutability - Configuration can't be accidentally modified
- Better DX - Auto-discovery reduces boilerplate
The main function to render components:
render_component(
component_name: str, # Name of component (e.g., 'button')
props: Dict[str, Any] = None, # Variables for template
# Directory overrides (optional)
templates_dir: str = None,
styles_dir: str = None,
scripts_dir: str = None,
# Asset control (optional)
styles: List[str] = None, # CSS files to load
scripts: List[str] = None, # JS files to load
frameworks: List[str] = None, # External frameworks
# Display options
height: int = None, # Component height in pixels
width: int = None,
scrolling: bool = False,
key: str = None, # Streamlit component key
# Performance
cache: bool = None, # Enable caching
cache_ttl: int = None, # Cache time-to-live (seconds)
# Interactivity
on_event: Callable = None # Callback for JS events
)Set global defaults for all components:
from streamlit_html_components import configure
configure(
templates_dir='components/templates',
styles_dir='components/styles',
scripts_dir='components/scripts',
default_cache=True,
external_frameworks=['tailwind']
)Add custom CSS/JS frameworks:
from streamlit_html_components import add_framework
add_framework(
'my_framework',
css_urls=['https://cdn.example.com/framework.css'],
js_urls=['https://cdn.example.com/framework.js']
)<h1>{{ title }}</h1>
<p>{{ description }}</p>{% if user_logged_in %}
<button>Logout</button>
{% else %}
<button>Login</button>
{% endif %}<ul>
{% for item in items %}
<li>{{ item.name }}: {{ item.price | currency }}</li>
{% endfor %}
</ul>Built-in filters:
{{ price | currency }}→ "$1,234.56"{{ date | date("%B %d, %Y") }}→ "December 10, 2025"{{ 0.156 | percentage }}→ "15.6%"
base.html:
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>button.html:
{% extends "base.html" %}
{% block title %}My Button{% endblock %}
{% block content %}
<button>{{ text }}</button>
{% endblock %}configure(external_frameworks=['tailwind'])
render_component('card', props={
'title': 'Product Card',
'price': 99.99
})card.html:
<div class="max-w-sm rounded-lg shadow-lg bg-white p-6">
<h2 class="text-2xl font-bold text-gray-800">{{ title }}</h2>
<p class="text-3xl text-indigo-600">{{ price | currency }}</p>
</div>configure(external_frameworks=['bootstrap'])configure(external_frameworks=['bulma'])add_framework(
'my_ui_kit',
css_urls=['https://cdn.example.com/ui-kit.css'],
js_urls=['https://cdn.example.com/ui-kit.js']
)
configure(external_frameworks=['my_ui_kit'])Components are cached by default for optimal performance:
# Enable caching (default)
render_component('button', props={'text': 'Click'}, cache=True)
# Set cache expiration (30 seconds)
render_component('button', props={'text': 'Click'}, cache_ttl=30)
# Disable caching for dynamic content
render_component('live_data', props={'data': data}, cache=False)
# Clear cache manually
from streamlit_html_components import invalidate_cache
invalidate_cache('button') # Clear specific component
invalidate_cache() # Clear all cacheCache Statistics:
from streamlit_html_components import cache_stats
stats = cache_stats()
# {'total_entries': 5, 'total_size_kb': 45.2, ...}Enable instant component updates during development - no Streamlit restart needed!
from streamlit_html_components import configure_v2, enable_hot_reload
# Configure your components
configure_v2(
templates_dir='components/templates',
styles_dir='components/styles',
scripts_dir='components/scripts'
)
# Enable hot reload with one line!
enable_hot_reload(verbose=True)
# Now edit your component files and see changes instantly! 🔥What gets watched:
templates/*.html- Component templatesstyles/*.css- CSS stylesheetsscripts/*.js- JavaScript files
How it works:
- FileWatcher detects file changes (using watchdog or polling)
- DevServer invalidates cache for affected components
- Streamlit auto-reruns and shows updated component
- Zero manual intervention required!
Optional dependency:
pip install watchdog # For instant file detection (recommended)
# Without watchdog, falls back to polling mode (still works!)Send data from JavaScript to Python:
button.js:
document.getElementById('myBtn').addEventListener('click', function() {
// Send event to Python
window.sendToStreamlit('click', {
clicks: clickCount,
timestamp: new Date().toISOString()
});
});app.py:
def on_button_click(data):
st.write(f"Received: {data}")
st.session_state.clicks = data['clicks']
render_component(
'button',
props={'text': 'Click me'},
on_event=on_button_click # Python callback
)Real-time state synchronization with conflict resolution:
from streamlit_html_components.bidirectional import StateManager, ConflictResolution
# Create state manager
state_manager = StateManager(
conflict_resolution=ConflictResolution.MERGE,
max_history=100
)
# Set initial state
state_manager.set_state('counter', {'count': 0, 'step': 1})
# Subscribe to state changes
def on_state_change(snapshot):
st.write(f"State updated: {snapshot.state}")
state_manager.subscribe('counter', on_state_change)
# Sync from client (JavaScript)
success, conflicts = state_manager.sync_from_client(
'counter',
client_state={'count': 5},
client_version=1
)Conflict Resolution Strategies:
CLIENT_WINS- Client updates always acceptedSERVER_WINS- Server state maintainedLATEST_WINS- Most recent update winsMERGE- Intelligent merging of changesCUSTOM- User-defined resolution function
State Features:
- Version tracking for every state change
- Complete state history with rollback
- State diffing (added/modified/removed)
- Export/import as JSON
- Real-time change notifications
Record and replay component events for debugging and testing:
from streamlit_html_components.bidirectional import get_bridge
bridge = get_bridge()
# Events are automatically recorded
bridge.handle_event('my_component', {
'event': 'click',
'data': {'button_id': 'submit'}
})
# Get event history
events = bridge.get_event_history('my_component')
# Replay all events
bridge.replay_events('my_component')
# Export events as JSON
json_data = bridge.export_events('my_component')Validate component props with JSON Schema or manual rules:
from streamlit_html_components import register_component, PropsSchema
# Define schema
schema = PropsSchema({
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1},
"age": {"type": "integer", "minimum": 0},
"email": {"type": "string", "format": "email"}
},
"required": ["name", "email"]
})
# Register component with validation
register_component(
name='user_form',
template='user_form.html',
props_schema=schema
)
# Invalid props will raise InvalidPropsError
render_component_v2('user_form', props={
'name': '', # ❌ Fails: minLength validation
'age': -5, # ❌ Fails: minimum validation
})from streamlit_html_components import PropsSchema, ValidationRule, ValidationType
schema = PropsSchema()
# Add validation rules
schema.add_rule(ValidationRule(
prop_name='email',
validation_type=ValidationType.PATTERN,
rule=r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
error_message='Must be a valid email'
))
schema.add_rule(ValidationRule(
prop_name='age',
validation_type=ValidationType.RANGE,
rule={'min': 0, 'max': 120},
error_message='Age must be between 0 and 120'
))
# Validate props
is_valid, errors = schema.validate({'email': 'test@example.com', 'age': 25})Validation Types:
REQUIRED- Field must be presentTYPE- Value type checking (str, int, float, bool, list, dict)PATTERN- Regex pattern matchingRANGE- Min/max value validationENUM- Value must be in allowed listCUSTOM- Custom validation function
render_component(
'complex_component',
styles=['common', 'component', 'theme'],
scripts=['utils', 'component', 'init']
)# Skip CSS loading
render_component('text_only', styles=[])
# Skip JS loading
render_component('static_card', scripts=[])import streamlit as st
user_name = st.text_input("Your name")
user_email = st.text_input("Your email")
render_component('profile_card', props={
'name': user_name,
'email': user_email,
'joined_date': datetime.now()
})The package includes complete working examples:
-
Basic Button (
examples/basic_button/)- Simple component with HTML/CSS/JS
- Click counting and callbacks
- Component customization
-
Tailwind Card (
examples/tailwind_card/)- External framework integration
- Product card with pricing
- Interactive builder
-
Payslip Integration (
examples/payslip_integration/)- Real-world use case
- Integration with existing Streamlit app
To run examples:
cd examples/basic_button
streamlit run app.pyBy default, render_component('button') automatically loads:
templates/button.html(required)styles/button.css(optional)scripts/button.js(optional)
render_component(
'button',
styles=['button', 'animations'], # Load multiple CSS files
scripts=['button', 'utils'] # Load multiple JS files
)render_component(
'button',
templates_dir='custom/templates',
styles_dir='custom/styles',
scripts_dir='custom/scripts'
)- Automatic HTML escaping via Jinja2 auto-escaping
- Props validation and sanitization
- Path traversal protection
<!-- Automatically escaped -->
<div>{{ user_input }}</div>
<!-- Explicit unsafe (use carefully) -->
<div>{{ trusted_html | safe }}</div>| Function | Description |
|---|---|
configure_v2() |
Configure with Pydantic validation and auto-discovery |
render_component_v2() |
Render component with registry validation |
register_component() |
Manually register a component |
list_components() |
List all registered components |
get_component_info() |
Get component metadata |
get_config_v2() |
Get current v2 configuration |
get_registry() |
Get component registry instance |
| Function | Description |
|---|---|
render_component() |
Render an HTML component |
configure() |
Set global configuration |
add_framework() |
Add custom framework |
| Function | Description |
|---|---|
invalidate_cache() |
Clear component cache |
cache_stats() |
Get cache statistics |
| Function | Description |
|---|---|
get_config() |
Get current configuration |
reset_config() |
Reset to defaults |
| Exception | Raised When |
|---|---|
ComponentNotFoundError |
Template file not found |
AssetNotFoundError |
CSS/JS file not found |
TemplateSyntaxError |
Template syntax error |
InvalidPropsError |
Props validation fails |
ConfigurationError |
Invalid configuration |
# Check configuration
from streamlit_html_components import get_config
config = get_config()
print(config)
# Verify file paths
import os
print(os.listdir('templates'))# Explicit file specification
render_component(
'button',
styles=['button'], # Explicitly specify CSS file
scripts=['button'] # Explicitly specify JS file
)# Clear cache
from streamlit_html_components import invalidate_cache
invalidate_cache()
# Disable caching for debugging
render_component('button', cache=False)Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Built with Streamlit
- Template engine powered by Jinja2
- Inspired by the need for traditional web development workflows in Streamlit
- Documentation: GitHub Repository
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Made with ❤️ for the Streamlit community