The fastest way to create HTML apps - a next-generation Python web framework for building fast, scalable web applications with minimal code
—
User notification system with toast messages, flash messages, and session-based notification management for enhanced user experience.
Create styled toast notifications with different types and behaviors.
def Toast(message: str, typ: str = "info", dismiss: bool = False, duration: int = 5000):
"""
Create toast notification message.
Generates a toast notification with specified type, styling,
and behavior options including auto-dismiss functionality.
Args:
message: Notification message text
typ: Toast type ('info', 'success', 'warning', 'error')
dismiss: Whether toast can be manually dismissed
duration: Auto-dismiss duration in milliseconds (0 = no auto-dismiss)
Returns:
Toast notification element with styling and behavior
"""Manage toast notifications through user sessions for persistence across requests.
def add_toast(sess, message: str, typ: str = "info", dismiss: bool = False):
"""
Add toast notification to user session.
Stores toast notification in session for display on next
page load or HTMX response.
Args:
sess: User session object
message: Notification message
typ: Toast type ('info', 'success', 'warning', 'error')
dismiss: Whether toast can be dismissed
"""
def render_toasts(sess):
"""
Render all pending toast notifications from session.
Retrieves and renders all stored toast notifications,
then clears them from the session.
Args:
sess: User session object
Returns:
Collection of toast notification elements
"""Integrate toast system into FastHTML applications with automatic rendering.
def setup_toasts(app, duration: int = 5000):
"""
Set up toast notification system in FastHTML app.
Configures automatic toast rendering and JavaScript
integration for a complete notification system.
Args:
app: FastHTML application instance
duration: Default toast duration in milliseconds
"""
def toast_after(resp, req, sess):
"""
After-request handler for toast notifications.
Automatically adds pending toasts to responses,
ensuring notifications are displayed to users.
Args:
resp: HTTP response object
req: HTTP request object
sess: User session object
Returns:
Modified response with toast notifications
"""from fasthtml.common import *
app, rt = fast_app(secret_key='demo-key')
# Set up toast system
setup_toasts(app, duration=4000)
@rt('/')
def homepage():
return Titled("Toast Notifications Demo",
Container(
H1("Toast Notification System"),
P("Click the buttons below to see different types of notifications."),
Div(
Button(
"Show Info Toast",
hx_post="/toast/info",
hx_target="#toast-area",
hx_swap="beforeend",
cls="primary"
),
Button(
"Show Success Toast",
hx_post="/toast/success",
hx_target="#toast-area",
hx_swap="beforeend",
cls="secondary"
),
Button(
"Show Warning Toast",
hx_post="/toast/warning",
hx_target="#toast-area",
hx_swap="beforeend"
),
Button(
"Show Error Toast",
hx_post="/toast/error",
hx_target="#toast-area",
hx_swap="beforeend"
),
style="display: flex; gap: 1rem; margin: 2rem 0;"
),
# Toast display area
Div(id="toast-area", style="position: fixed; top: 1rem; right: 1rem; z-index: 1000;")
)
)
@rt('/toast/info', methods=['POST'])
def show_info_toast():
return Toast("This is an info message!", typ="info", dismiss=True)
@rt('/toast/success', methods=['POST'])
def show_success_toast():
return Toast("Operation completed successfully!", typ="success", dismiss=True)
@rt('/toast/warning', methods=['POST'])
def show_warning_toast():
return Toast("Warning: Please check your input!", typ="warning", dismiss=True)
@rt('/toast/error', methods=['POST'])
def show_error_toast():
return Toast("Error: Something went wrong!", typ="error", dismiss=True, duration=0) # No auto-dismiss for errorsfrom fasthtml.common import *
app, rt = fast_app(secret_key='demo-key')
setup_toasts(app)
@rt('/')
def form_page(request):
# Render any pending toasts
toasts = render_toasts(request.session)
return Titled("Form with Notifications",
Container(
H1("User Registration Form"),
# Display toast area
Div(*toasts, id="toast-container"),
Form(
Div(
Label("Username:", for_="username"),
Input(type="text", name="username", id="username", required=True),
cls="form-group"
),
Div(
Label("Email:", for_="email"),
Input(type="email", name="email", id="email", required=True),
cls="form-group"
),
Div(
Label("Password:", for_="password"),
Input(type="password", name="password", id="password", required=True),
cls="form-group"
),
Button("Register", type="submit"),
method="post",
action="/register"
)
)
)
@rt('/register', methods=['POST'])
def register_user(username: str, email: str, password: str, request):
# Simulate validation
if len(username) < 3:
add_toast(request.session, "Username must be at least 3 characters long", "error")
return Redirect('/')
if len(password) < 6:
add_toast(request.session, "Password must be at least 6 characters long", "error")
return Redirect('/')
# Simulate checking if user exists
if username.lower() in ['admin', 'root', 'test']:
add_toast(request.session, f"Username '{username}' is already taken", "warning")
return Redirect('/')
# Simulate successful registration
add_toast(request.session, f"Welcome {username}! Your account has been created successfully.", "success")
add_toast(request.session, "Please check your email to verify your account.", "info")
return Redirect('/dashboard')
@rt('/dashboard')
def dashboard(request):
toasts = render_toasts(request.session)
return Titled("Dashboard",
Container(
Div(*toasts, id="toast-container"),
H1("User Dashboard"),
P("Welcome to your dashboard!"),
Div(
Button(
"Save Settings",
hx_post="/save-settings",
hx_target="#toast-container",
hx_swap="innerHTML"
),
Button(
"Delete Account",
hx_post="/delete-account",
hx_target="#toast-container",
hx_swap="innerHTML",
hx_confirm="Are you sure you want to delete your account?"
)
)
)
)
@rt('/save-settings', methods=['POST'])
def save_settings(request):
add_toast(request.session, "Settings saved successfully!", "success")
return render_toasts(request.session)
@rt('/delete-account', methods=['POST'])
def delete_account(request):
add_toast(request.session, "Account deletion failed. Please contact support.", "error")
return render_toasts(request.session)from fasthtml.common import *
app, rt = fast_app(secret_key='demo-key')
# Custom toast setup with styling
def custom_toast_setup():
toast_styles = Style("""
.toast-container {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 1000;
max-width: 400px;
}
.toast {
margin-bottom: 0.5rem;
padding: 0.75rem 1rem;
border-radius: 0.375rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
animation: slideIn 0.3s ease-out;
position: relative;
overflow: hidden;
}
.toast-info {
background-color: #dbeafe;
border-left: 4px solid #3b82f6;
color: #1e40af;
}
.toast-success {
background-color: #dcfce7;
border-left: 4px solid #22c55e;
color: #15803d;
}
.toast-warning {
background-color: #fef3c7;
border-left: 4px solid #f59e0b;
color: #92400e;
}
.toast-error {
background-color: #fee2e2;
border-left: 4px solid #ef4444;
color: #dc2626;
}
.toast-dismiss {
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
opacity: 0.7;
margin-left: 1rem;
}
.toast-dismiss:hover {
opacity: 1;
}
.toast-progress {
position: absolute;
bottom: 0;
left: 0;
height: 3px;
background-color: rgba(0, 0, 0, 0.2);
animation: progress linear;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes progress {
from { width: 100%; }
to { width: 0%; }
}
.toast-auto-dismiss {
animation: slideOut 0.3s ease-in forwards;
animation-delay: var(--dismiss-delay, 4.7s);
}
@keyframes slideOut {
to {
transform: translateX(100%);
opacity: 0;
margin-bottom: -100px;
}
}
""")
toast_script = Script("""
function createToast(message, type = 'info', dismiss = true, duration = 5000) {
const container = document.getElementById('toast-container') ||
(() => {
const div = document.createElement('div');
div.id = 'toast-container';
div.className = 'toast-container';
document.body.appendChild(div);
return div;
})();
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
const messageSpan = document.createElement('span');
messageSpan.textContent = message;
toast.appendChild(messageSpan);
if (dismiss) {
const dismissBtn = document.createElement('button');
dismissBtn.className = 'toast-dismiss';
dismissBtn.innerHTML = '×';
dismissBtn.onclick = () => removeToast(toast);
toast.appendChild(dismissBtn);
}
if (duration > 0) {
const progress = document.createElement('div');
progress.className = 'toast-progress';
progress.style.animationDuration = `${duration}ms`;
toast.appendChild(progress);
setTimeout(() => removeToast(toast), duration);
}
container.appendChild(toast);
return toast;
}
function removeToast(toast) {
toast.style.animation = 'slideOut 0.3s ease-in forwards';
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}
// Auto-remove toasts with auto-dismiss class
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.toast-auto-dismiss').forEach(toast => {
const duration = parseInt(toast.dataset.duration) || 5000;
setTimeout(() => removeToast(toast), duration);
});
});
""")
return [toast_styles, toast_script]
def CustomToast(message: str, typ: str = "info", dismiss: bool = True, duration: int = 5000):
"""Custom toast with enhanced styling and behavior."""
toast_class = f"toast toast-{typ}"
if duration > 0:
toast_class += " toast-auto-dismiss"
elements = [Span(message)]
if dismiss:
elements.append(
Button("×",
cls="toast-dismiss",
onclick="removeToast(this.parentElement)")
)
if duration > 0:
elements.append(
Div(cls="toast-progress",
style=f"animation-duration: {duration}ms;")
)
return Div(*elements,
cls=toast_class,
data_duration=str(duration) if duration > 0 else None)
@rt('/')
def advanced_toast_demo():
return Html(
Head(
Title("Advanced Toast System"),
Meta(charset="utf-8"),
Meta(name="viewport", content="width=device-width, initial-scale=1"),
*custom_toast_setup()
),
Body(
Container(
H1("Advanced Toast Notification System"),
Div(
Button(
"Custom Info Toast",
onclick="createToast('This is a custom info message!', 'info', true, 4000)"
),
Button(
"Persistent Error Toast",
onclick="createToast('This error stays until dismissed!', 'error', true, 0)"
),
Button(
"Quick Success Toast",
onclick="createToast('Quick success!', 'success', false, 2000)"
),
Button(
"Warning with Long Message",
onclick="createToast('This is a longer warning message that demonstrates how the toast system handles extended content gracefully.', 'warning', true, 6000)"
),
style="display: flex; flex-direction: column; gap: 1rem; max-width: 300px;"
),
# Server-side toast examples
Div(
H2("Server-Side Toasts"),
Button(
"HTMX Success Toast",
hx_post="/custom-toast/success",
hx_target="#toast-container",
hx_swap="beforeend"
),
Button(
"HTMX Error Toast",
hx_post="/custom-toast/error",
hx_target="#toast-container",
hx_swap="beforeend"
),
style="margin-top: 2rem;"
)
),
# Toast container
Div(id="toast-container", cls="toast-container")
)
)
@rt('/custom-toast/success', methods=['POST'])
def custom_success_toast():
return CustomToast("Server-side success notification!", "success", True, 3000)
@rt('/custom-toast/error', methods=['POST'])
def custom_error_toast():
return CustomToast("Server-side error notification!", "error", True, 0)from fasthtml.common import *
app, rt = fast_app(secret_key='demo-key')
setup_toasts(app)
@rt('/')
def validation_form(request):
toasts = render_toasts(request.session)
return Titled("Form Validation with Toasts",
Container(
Div(*toasts, id="toast-container"),
H1("User Profile Form"),
Form(
Div(
Label("Full Name:", for_="name"),
Input(type="text", name="name", id="name", required=True),
cls="form-group"
),
Div(
Label("Email:", for_="email"),
Input(type="email", name="email", id="email", required=True),
cls="form-group"
),
Div(
Label("Age:", for_="age"),
Input(type="number", name="age", id="age", min="18", max="100"),
cls="form-group"
),
Div(
Label("Bio:", for_="bio"),
Textarea(name="bio", id="bio", rows="4", maxlength="500"),
cls="form-group"
),
Button("Save Profile", type="submit"),
hx_post="/validate-profile",
hx_target="#toast-container",
hx_swap="innerHTML"
)
)
)
@rt('/validate-profile', methods=['POST'])
def validate_profile(name: str, email: str, age: int = None, bio: str = "", request):
errors = []
warnings = []
# Validation logic
if len(name.strip()) < 2:
errors.append("Name must be at least 2 characters long")
if not email or '@' not in email:
errors.append("Please provide a valid email address")
if age is not None:
if age < 18:
errors.append("You must be at least 18 years old")
elif age > 100:
warnings.append("Please verify your age is correct")
if len(bio) > 500:
errors.append("Bio must be 500 characters or less")
elif len(bio) < 10 and bio:
warnings.append("Consider adding more details to your bio")
# Create response toasts
toasts = []
if errors:
for error in errors:
toasts.append(Toast(error, "error", dismiss=True, duration=0))
elif warnings:
for warning in warnings:
toasts.append(Toast(warning, "warning", dismiss=True, duration=8000))
toasts.append(Toast("Profile saved with warnings", "success", dismiss=True))
else:
toasts.append(Toast("Profile saved successfully!", "success", dismiss=True))
if not bio:
toasts.append(Toast("Tip: Add a bio to complete your profile", "info", dismiss=True))
return toastsInstall with Tessl CLI
npx tessl i tessl/pypi-python-fasthtml