Dars Framework is a full-stack Python UI framework for building web and desktop applications. It provides a declarative component-based architecture where developers define UIs in Python, which are then compiled to optimized HTML, CSS, and JavaScript. The framework supports multiple deployment targets from a single codebase: Single-Page Applications (SPA), Multi-Page Applications (MPA), Server-Side Rendered (SSR) applications with FastAPI, and desktop applications via Electron.
If you are new to Dars, we recommend following the learning path in this order:
dars
command to init, build, and export projects.
useDynamic
,
useValue
, and other hooks for modern component behavior.
dars.config.json
and environmental variables.
tst/
directory in the repository for hundreds of usage examples.
To install Dars, simply use pip:
pip install dars-framework
This will install Dars and all its dependencies automatically.
You can install the official Dars Framework VS Code extension to have the dars dev tools.
Once installed, the
dars
command will be available in your terminal. You can use it to:
dars export my_app.py --format html --output ./my_app_web
dars preview ./my_app_web
# Basic project with Hello World
dars init my_new_project
# Project with a specific template
dars init my_project -t basic/Forms
dars info my_app.py
dars formats
To verify that Dars has been installed correctly, open your terminal and run:
dars --help
You should see the help for the
dars
command, indicating that the installation was successful.
from dars.core.app import App
from dars.components.basic.text import Text
from dars.components.basic.container import Container
app = App(title="My First App")
container = Container(style="p-5") # Use ' to escape quotes
text = Text(text="Hello Dars!", style="text-2xl") # Use ' to escape quotes
container.add_child(text)
app.set_root(container)
if __name__ == "__main__":
app.rTimeCompile()
Save the code above as
my_first_app.py
and then run:
dars export my_first_app.py --format html --output ./my_app
dars preview ./my_app
# General help
dars --help
# Application information
dars info my_app.py
# Available formats
dars formats
# Preview application
dars preview ./output_directory
pip install dars-framework
dars
works correctly (
dars --help
)
Congratulations! Dars is ready to use.
You can build native desktop apps from Dars projects. This capability is in BETA and is not recommended for production yet, but it is usable for testing.
# Scaffold or update a desktop-capable project
dars init --type desktop
# or
dars init --update
# Verify optional tooling (Node/Bun and packager)
dars doctor --all --yes
# Ensure your config sets the desktop format and target
# dars.config.json
{
"entry": "main.py",
"format": "desktop",
"outdir": "dist",
"targetPlatform": "auto"
}
# Build desktop artifacts
dars build
Notes:
Welcome to Dars, a modern Python framework for building web applications with reusable UI components.
There is an official Dars Framework extension for VS Code to have the dars dev tools.
Install Dars
See INSTALL section for installation instructions.
Explore Components
Discover all available UI components in
components.md
.
Command-Line Usage
Find CLI commands, options, and workflows in
cli.md
.
App Class Learn how to create an app class in App Documentation .
Component Search and Modification All components in Dars now support a powerful search and modification system:
from dars.all import *
app = App(title="Hello World", theme="dark")
# 1. Define State
state = State("app", title_val="Simple Counter", count=0)
# 2. Define Route
@route("/")
def index():
return Page(
# 3. Use useValue for app text
Text(
text=useValue("app.title_val"),
style="fs-[33px] text-black font-bold mb-[5x] ",
),
# 4. Display reactive count
Text(
text=useDynamic("app.count"),
style="fs-[48px] mt-5 mb-[12px]"
),
# 5. Interactive Button
Button(
text="+1",
on_click=(
state.count.increment(1)
),
style="bg-[#3498db] text-white p-[15px] px-[30px] rounded-[8px] border-none cursor-pointer fs-[18px]",
),
# 6. Interactive Button
Button(
text="-1",
on_click=(
state.count.decrement(1)
),
style="bg-[#3498db] text-white p-[15px] px-[30px] rounded-[8px] border-none cursor-pointer fs-[18px] mt-[5px]",
),
# 7. Interactive Button
Button(
text="Reset",
on_click=(
state.reset()
),
style="bg-[#3498db] text-white p-[15px] px-[30px] rounded-[8px] border-none cursor-pointer fs-[18px] mt-[5px]",
),
style="flex flex-col items-center justify-center h-[100vh] ffam-[Arial] bg-[#f0f2f5]",
)
# 8. Add page
app.add_page("index", index(), title="index")
if __name__ == "__main__":
app.rTimeCompile(add_file_types=".js,.css")
Start building with Dars...
The Dars Command Line Interface (CLI) lets you manage your projects, export apps, and preview results quickly from the terminal.
Open your terminal in your project directory and use any of the following commands:
# Show information about your app
dars info my_app.py
# Export to different formats (web)
dars export my_app.py --format html --output ./output
# Skip default Python minifier for this run (does not affect viteMinify)
dars export my_app.py --format html --output ./output --no-minify
# List supported export formats
dars formats
# Initialize a new project (Default is SPA)
dars init my_new_project
# Initialize a Full-Stack SSR project
dars init my_new_project --type ssr
# Initialize a project with a specific template
dars init my_new_project -t demo/complete_app
# Preview an exported app
dars preview ./output_directory
# Build using project config (dars.config.json)
dars build
# Build desktop (BETA) when format is desktop in config
dars build
# Build without the default Python minifier
dars build --no-minify
# Help
dars --help
# Version
dars -v
| Command | What it does |
|---|---|
dars export my_app.py --format html
|
Export app to HTML/CSS/JS in
./my_app_web
|
dars export my_app.py --format html --no-minify
|
Export skipping default Python minifier |
dars preview ./my_app_web
|
Preview exported app locally |
dars build
|
Build using dars.config.json |
dars init --type desktop
|
Scaffold desktop-capable project (BETA) |
dars build
(desktop config)
|
Build desktop app artifacts (BETA) |
dars build --no-minify
|
Build skipping default Python minifier |
dars init my_project --type ssr
|
Create a new Full-Stack SSR project |
dars init my_project
|
Create a new Dars project (SPA Default) |
dars dev
|
Run the configured entry file with hot preview (app.rTimeCompile) |
dars dev --backend
|
Run only the configured backendEntry (FastAPI/SSR backend) |
dars info my_app.py
|
Show info about your app |
dars formats
|
List supported export formats |
dars --help
|
Show help and all CLI options |
Dars provides official templates to help you start new projects quickly. Templates include ready-to-use apps for forms, layouts, dashboards, multipage, and more.
dars init my_new_project -t basic/HelloWorld
# ...and more (see below)
You can see the templates available with
dars init --list-templates
dars init -L
Export the template to HTML/CSS/JS:
dars export main.py --format html --output ./hello_output
dars export main.py --format html --output ./dashboard_output
# ...etc
Preview the exported app:
dars preview ./hello_output
dars --help
for a full list of commands and options.
app.rTimeCompile()
) or from exported files with
dars preview
.
dars init my_project -t <template>
.
"format": "desktop"
in
dars.config.json
.
dars init --type desktop
(or
--update
) to scaffold backend files.
dars doctor --all --yes
to set up optional tooling.
dars build
. This feature is in BETA: suitable for testing, not yet for production.
For more, see the Getting Started guide and the main documentation index.
When working with SSR (
dars init --type ssr
), the workflow involves two processes:
dars dev
)
: Runs the Dars preview server on port
8000
using
app.rTimeCompile()
.
dars dev --backend
)
: Runs the FastAPI SSR backend on port
3000
using
backendEntry
from
dars.config.json
.
Common Commands:
| Command | Description |
|---|---|
dars init --type ssr
|
Scaffolds a project with
backend/
folder and SSR config.
|
dars dev
|
Starts the hot-reload frontend preview server. |
dars dev --backend
|
Starts the SSR/backend server defined by
backendEntry
.
|
dars build
|
Builds static assets to
dist/
for production.
|
Note: For production, you only need to run the backend (which serves the built assets).
The
dars.config.json
file is the central nervous system of your Dars project. it defines how your application is compiled, optimized, and prepared for deployment.
A standard configuration file looks like this:
{
"entry": "main.py",
"format": "html",
"outdir": "dist",
"publicDir": "public",
"include": [],
"exclude": ["**/__pycache__", ".git", ".venv", "node_modules"],
"bundle": true,
"defaultMinify": true,
"viteMinify": true,
"markdownHighlight": true,
"markdownHighlightTheme": "auto",
"utility_styles": {},
"backendEntry": "backend.api:app"
}
entry
(string): The Python entry point of your application. Defaults to
main.py
.
format
(string): Target deployment format.
html
: Standard web application (SPA/MPA/SSR).
desktop
: Native desktop application (Electron-based, BETA).
outdir
(string): The directory where compiled assets will be saved. Defaults to
dist
.
publicDir
(string): Directory for static assets (images, fonts, etc.) that will be copied directly to the output.
include
(list): List of glob patterns or substrings to include from the public directory.
exclude
(list): List of patterns to ignore during the build process.
Dars features a multi-stage minification pipeline to ensure the smallest possible production bundle.
defaultMinify
(boolean): Controls the internal Python-side HTML/CSS minifier.
viteMinify
(boolean): Enables advanced JavaScript minification using Vite/esbuild. Recommended for production.
bundle
(boolean): Ensures all internal dependencies are bundled into the final distribution.
markdownHighlight
(boolean): Automatically injects Prism.js for syntax highlighting in Markdown components.
backendEntry
(string): Import path for your FastAPI backend (e.g.,
"backend.api:app"
). Required for SSR projects.
One of Dars' most powerful features is the ability to define custom utility classes directly in the configuration. This allows you to create reusable design tokens.
{
"utility_styles": {
"btn-primary": [
"bg-blue-600",
"text-white",
"px-4",
"py-2",
"rounded-lg",
"hover:bg-blue-700",
"transition-all"
],
"card-glass": [
"bg-white/30",
"backdrop-blur-md",
"border",
"border-white/20",
"rounded-2xl",
"shadow-xl"
]
}
}
Usage in Python:
Button("Click Me", style="btn-primary")
Container(style="card-glass")
The standard format for deploying to the web. When
format
is
html
, Dars generates optimized HTML, CSS, and JS files compatible with any static host or FastAPI server.
When
format
is
desktop
, Dars produces native desktop artifacts.
-
targetPlatform
(string): Specifies the target OS (
auto
,
windows
,
linux
,
macos
).
publicDir
to speed up build times.
dars env
system to manage different configurations for development and production.
dars config validate
to ensure your configuration matches the requirements of your chosen route types (especially for SSR).
Dars provides a simple, built-in way to detect the current running environment (Development vs Production) through the
DarsEnv
class.
It is common to have logic that should only run during development (like showing debug tools, detailed logs, or specific navigation links) or only in production (like analytics scripts).
The
DarsEnv
class is available directly from
dars.env
:
from dars.env import DarsEnv
DarsEnv.dev
A boolean property that indicates if the application is running in development mode.
True
: When running via
dars dev
(where bundling is disabled by default).
False
: When running via
dars build
or
dars export
(production builds).
You can use
DarsEnv.dev
to conditionally render components:
from dars.all import *
from dars.env import DarsEnv
def MyPage():
return Page(
Container(
Text("Welcome to My App"),
# This link only appears in development
Link("/debug-dashboard", "Debug Tools") if DarsEnv.dev else None,
# Different footer for environments
Text("Dev Mode: Active" if DarsEnv.dev else "Production Build")
)
)
You can also use it to toggle configuration values:
api_url = "http://localhost:3000" if DarsEnv.dev else "https://api.myapp.com"
Note :
DarsEnvis initialized automatically by the Dars CLI tools. You do not need to configure it manually.
The
App
class is the core of any Dars Framework application. It represents the complete application and manages all configuration, components, pages, and functionalities, including Progressive Web App (PWA) support.
class App:
def __init__(
self,
title: str = "Dars App",
description: str = "",
author: str = "",
keywords: List[str] = None,
language: str = "en",
favicon: str = "",
icon: str = "",
apple_touch_icon: str = "",
manifest: str = "",
theme_color: str = "#000000",
background_color: str = "#ffffff",
service_worker_path: str = "",
service_worker_enabled: bool = False,
**config
):
Dars lets you modify the component tree before export or preview. Use these methods on
App
to insert or remove components safely at compile time.
target
can be:
Button("OK", id="ok")
).
str
id of an existing component in the app tree to move it.
root
: Where to insert. Accepts:
str
).
str
) in multipage apps.
on_top_of
/
on_bottom_of
: Reference sibling inside
root
to place the new node before/after. Can be an id (
str
) or a component instance found within
root
(deep search). If neither is provided, it appends to
root
.
app.root
) and multipage (
app.add_page
) setups.
from dars.all import *
app = App(title="Compile-time create/delete")
# Single-page usage
root = Container(Text("A" , id="a"), Text("C", id="c"), id="root")
app.set_root(root)
# Insert new Text before id="c"
app.create(Text("B", id="b"), root="root", on_top_of="c")
# Move existing component by id to bottom
app.create("a", root="root", on_bottom_of="c")
# Multipage usage (root by page name)
home = Page(Container(Text("Home"), id="home_root"))
app.add_page(name="home", root=home, index=True)
app.create(Text("Welcome", id="welcome"), root="home", on_bottom_of="home_root")
id
.
# Remove a component by id before export/preview
app.delete("b")
These operations run before export and affect the generated HTML/VDOM. For dynamic changes at runtime in the browser, see the runtime APIs in the Components documentation.
The
App
class now includes an enhanced
add_global_style()
method that supports both inline style definitions and external CSS file imports.
1. Inline Style Definition (Traditional)
app.add_global_style(
selector=".my-button",
styles={
"background-color": "#4CAF50",
"color": "white",
"padding": "10px 20px",
"border-radius": "5px"
}
)
2. External CSS File Import (New in v1.1.2)
app.add_global_style(file_path="styles.css")
3. Combined Usage
# Add inline styles
app.add_global_style(
selector=".primary-btn",
styles={
"background-color": "#007bff",
"color": "white"
}
)
# Import external CSS file
app.add_global_style(file_path="components.css")
main.py:
from dars.all import *
app = App(title="Dars Styling Test")
index = Page(
Container(
Button("Styled Button", class_name="button-styling-test"),
id="page_sub_container"
)
)
app.add_page(name="index", root=index, title="index", index=True)
app.add_global_style(file_path="styles.css")
if __name__ == "__main__":
app.rTimeCompile(add_file_types=".py, .css")
styles.css:
.button-styling-test {
background-color: rgb(51, 255, 0);
padding: 15px 30px;
border-radius: 8px;
border: none;
font-weight: bold;
cursor: pointer;
}
.button-styling-test:hover {
background-color: rgb(30, 200, 0);
transform: scale(1.05);
}
#page_sub_container {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
}
When using
app.rTimeCompile()
with the
add_file_types=".css"
parameter, the development server automatically watches for changes in CSS files and reloads the application when modifications are detected.
# Watch for both Python and CSS file changes
app.rTimeCompile(add_file_types=".py, .css")
# Or watch for CSS files only
app.rTimeCompile(add_file_types=".css")
The enhanced method maintains full backward compatibility - existing code using
add_global_style(selector, styles)
will continue to work without modification.
The App class includes these PWA-specific properties:
# Icons and visual resources
self.favicon = favicon # Path to traditional favicon
self.icon = icon # Main icon for PWA (multiple sizes)
self.apple_touch_icon = apple_touch_icon # Icon for Apple devices
self.manifest = manifest # Path to manifest.json file
# Colors and theme
self.theme_color = theme_color # Theme color (#RRGGBB)
self.background_color = background_color # Background color for splash screens
# Service Worker
self.service_worker_path = service_worker_path # Path to service worker file
self.service_worker_enabled = service_worker_enabled # Enable/disable
# Additional PWA configuration
self.pwa_enabled = config.get('pwa_enabled', False)
self.pwa_name = config.get('pwa_name', title)
self.pwa_short_name = config.get('pwa_short_name', title[:12])
self.pwa_display = config.get('pwa_display', 'standalone')
self.pwa_orientation = config.get('pwa_orientation', 'portrait')
The App class provides methods to generate PWA meta tags:
def get_meta_tags(self) -> Dict[str, str]:
"""Returns all meta tags as a dictionary"""
meta_tags = {}
# Viewport configured for responsiveness
viewport_parts = []
for key, value in self.config['viewport'].items():
if key == 'initial_scale':
viewport_parts.append(f'initial-scale={value}')
elif key == 'user_scalable':
viewport_parts.append(f'user-scalable={value}')
else:
viewport_parts.append(f'{key.replace("_", "-")}={value}')
meta_tags['viewport'] = ', '.join(viewport_parts)
# Specific tags for PWA
meta_tags['theme-color'] = self.theme_color
if self.pwa_enabled:
meta_tags['mobile-web-app-capable'] = 'yes'
meta_tags['apple-mobile-web-app-capable'] = 'yes'
meta_tags['apple-mobile-web-app-status-bar-style'] = 'default'
meta_tags['apple-mobile-web-app-title'] = self.pwa_short_name
return meta_tags
The
HTMLCSSJSExporter
uses the PWA configuration from the App class to generate:
{
"name": "App Name",
"short_name": "Short Name",
"description": "Application description",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"orientation": "portrait",
"icons": [
{
"src": "icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
The exporter automatically generates code to register the service worker:
if ('serviceWorker' in navigator && '{service_worker_path}') {
window.addEventListener('load', function() {
navigator.serviceWorker.register('{service_worker_path}')
.then(function(registration) {
console.log('ServiceWorker registration successful');
})
.catch(function(error) {
console.log('ServiceWorker registration failed: ', error);
});
});
}
# Create a complete PWA application
app = App(
title="My PWA App",
description="An amazing progressive application",
author="My Company",
keywords=["pwa", "webapp", "productivity"],
language="en",
favicon="assets/favicon.ico",
icon="assets/icon-192x192.png",
apple_touch_icon="assets/apple-touch-icon.png",
theme_color="#4A90E2",
background_color="#FFFFFF",
service_worker_path="sw.js",
service_worker_enabled=True,
pwa_enabled=True,
pwa_name="My App",
pwa_short_name="MyApp",
pwa_display="standalone"
)
# Add pages and components
app.add_page("home", HomeComponent(), title="Home", index=True)
app.add_page("about", AboutComponent(), title="About")
Dars Framework's PWA implementation is compatible with: - Chrome/Chromium (full support) - Firefox (basic support) - Safari (limited support on iOS) - Edge (full support)
You can import all main components and modules with a single line:
from dars.all import *
This simplifies integration and improves developer experience by exposing components like
Text
,
Button
,
Container
,
State
, and DAP functions like
log
,
alert
,
showModal
, etc.
Note : Be careful if you create custom components with the same names as built-in components to avoid conflicts.
Components are the fundamental UI elements in Dars. Each component encapsulates its appearance, behavior, and state.
In modern Dars, you should heavily rely on
Utility Classes
(Tailwind-like classes) using the
class_name
property instead of the old
style
dictionary, and use
DAP Functions
(
show()
,
hide()
,
log()
,
alert()
,
updateVRef()
) instead of writing raw inline JavaScript for events.
For custom components, refer to Custom Components .
All UI elements inherit from the
Component
base class, which provides standard attributes and DOM manipulation methods.
"flex flex-col bg-slate-100 p-4 rounded-lg"
).
class_name
).
on_click
,
on_change
,
on_mouse_enter
, etc. Accept DAP utility functions or state setters.
The
Container
acts as a generic
<div>
to group components.
from dars.all import *
layout = Container(
Text("Welcome to Dars", style="text-4xl font-black text-slate-900 mb-4"),
Button("Get Started", style="bg-indigo-600 text-white px-6 py-3 rounded-xl shadow-lg hover:bg-indigo-700 transition-all scale-105 active:scale-95"),
style="flex flex-col items-center justify-center min-h-screen bg-slate-50"
)
Similar to
Container
, but renders as a semantic
<section>
HTML tag.
main_section = Section(
Text("About Us", style="text-xl font-semibold"),
style="p-8 border-t border-slate-200"
)
Displays static or reactive text.
title = Text(
"Dars Framework",
style="text-6xl font-black text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500"
)
# Reactive text using useDynamic
user_name = Text(useDynamic("user.name"), style="text-lg font-medium")
Creates interactive clickable buttons. Use DAP functions for events instead of inline JavaScript.
submit_btn = Button(
"Save Changes",
style="bg-emerald-500 text-white font-semibold py-3 px-6 rounded-lg transition-all hover:bg-emerald-600 hover:shadow-xl active:scale-95",
on_click=log("Changes saved successfully!")
)
Allows text or numeric user input.
email_input = Input(
placeholder="Enter your email",
input_type="email",
required=True,
style="w-full p-3 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500",
on_change=log("Email updated")
)
For boolean selections or single-choice groups.
terms = Checkbox(
label="I accept the terms",
checked=False,
style="accent-blue-500"
)
theme_radio = RadioButton(
label="Dark Mode",
name="theme_group",
checked=True
)
Dropdown menu for options.
dropdown = Select(
options=["Option 1", "Option 2", "Option 3"],
value="Option 1",
style="p-2 border rounded bg-white text-slate-700"
)
For multi-line inputs.
comment = Textarea(
placeholder="Write your comment...",
rows=4,
style="w-full p-2 border border-slate-300 rounded resize-y"
)
Range selection slider.
volume = Slider(
min_value=0, max_value=100, value=50,
style="w-[200px] accent-blue-500"
)
upload = FileUpload(
label="Upload Document",
accept=".pdf",
style="cursor-pointer bg-slate-100 p-2 rounded border-dashed border-2"
)
date = DatePicker(value="2025-01-01", style="p-2 rounded border")
logo = Image(
src="/assets/logo.png",
alt="Logo",
style="w-[120px] h-auto object-contain drop-shadow-md"
)
Wrappers over HTML5 media tags with reactivity support.
video_player = Video(
src="/media/promo.mp4",
controls=True,
autoplay=False,
style="w-full rounded-xl shadow-lg"
)
audio_player = Audio(
src="/media/soundtrack.mp3",
controls=True
)
nav_link = Link(
"Go to Dashboard",
href="/dashboard",
style="text-blue-500 hover:text-blue-700 underline"
)
A structured container for displaying related info.
user_card = Card(
title="Profile Info",
children=[Text("Username: ZtaDev"), Button("Edit")],
style="bg-white rounded-xl shadow-md p-6 max-w-sm border border-slate-100"
)
An overlay dialog. Modals are hidden by default to prevent flicker on load. Use
showModal()
and
hideModal()
DAP utilities to control them.
from dars.all import *
info_modal = Modal(
id="info-modal",
title="Information",
is_open=False,
children=[Text("Here are some details.")],
style="bg-white rounded-lg p-6 w-[400px]",
overlay_class="bg-black/50 backdrop-blur-sm"
)
# Open modal using the utility function
open_btn = Button("Show Info", on_click=showModal("info-modal"))
Top navigation bar structure.
nav = Navbar(
brand=Text("MyApp", style="text-xl font-bold"),
children=[Link("Home", "/"), Link("Settings", "/settings")],
style="flex justify-between items-center bg-slate-900 text-white p-4 sticky top-0 z-50"
)
For collapsing content or switching between views.
faq = Accordion(
items=[
{"title": "What is Dars?", "content": "A reactive Python web framework."},
{"title": "Is it fast?", "content": "Yes!"}
],
style="border rounded"
)
dashboard_tabs = Tabs(
tabs=[
{"label": "Overview", "content": Text("Overview data")},
{"label": "Metrics", "content": Text("Metrics data")}
]
)
Renders Markdown as HTML. Built-in support for highlight.js code highlighting.
doc = Markdown(
content="## Hello\nThis is **Markdown**",
style="prose prose-slate max-w-none p-4"
)
prog = ProgressBar(value=75, max_value=100, style="w-full h-2 bg-slate-200 rounded-full [&::-webkit-progress-value]:bg-blue-500")
tip = Tooltip(text="Click to save", child=Button("Save"))
Render tabular data from lists or Pandas DataFrames.
data = [
{'Name': 'Alice', 'Role': 'Admin'},
{'Name': 'Bob', 'Role': 'User'}
]
table = DataTable(data, striped=True, hover=True, theme="light", style="w-full text-left")
Renders Plotly.js charts.
import plotly.graph_objects as go
fig = go.Figure(data=[go.Bar(x=['A', 'B'], y=[10, 20])])
chart = Chart(figure=fig, style="w-full h-[400px]")
While you can use
Container
with CSS classes (
style="flex gap-4"
), Dars provides dedicated layout components for convenience.
row = FlexLayout(
direction="row",
justify="space-between",
align="center",
gap="16px",
children=[Button("Cancel"), Button("Confirm")],
style="w-full p-4"
)
grid = GridLayout(
rows=2, cols=2, gap="24px",
children=[Text("1"), Text("2"), Text("3"), Text("4")],
style="w-full max-w-4xl mx-auto"
)
Dars allows you to create and delete components dynamically at runtime, with full support for lifecycle hooks.
from dars.all import *
# Component to be generated
msg = Text("Dynamic Message", id="msg", style="text-emerald-600 font-medium")
# Creates the component inside the container with ID 'root'
create_btn = Button("Add Message", on_click=createComp(msg, root="root", position="append"))
# Deletes the component with ID 'msg'
delete_btn = Button("Remove Message", on_click=deleteComp("msg"))
Components can trigger DAP operations or callbacks when they are created, updated, or destroyed.
dynamic_box = Container(
id="dyn_box",
class_name="p-4 border rounded",
onMount=log("dyn_box was added to DOM"),
onUpdate=log("dyn_box was updated"),
onUnmount=log("dyn_box was removed from DOM")
)
updateComp
or reactive
V()
updates).
Dars Framework introduces a powerful, Python-native utility class system inspired by Tailwind CSS. This system allows you to style your components using concise string utilities directly in your Python code, without needing Node.js, PostCSS, or any external build tools.
Tip: The official Dars Framework VS Code extension provides Tailwind-like utility style completions while editing
style="..."strings in Python.
- VS Code Marketplace: https://marketplace.visualstudio.com/items?itemName=ZtaMDev.dars-framework
- Open VSX: https://open-vsx.org/extension/ztamdev/dars-framework
Instead of writing raw CSS dictionaries or separate CSS files, you can now use the
style
,
hover_style
, and
active_style
arguments with utility strings. These strings are parsed at runtime (and export time) into standard CSS dictionaries.
from dars.all import *
def MyComponent():
return Container(
Text("Hello, Dars!"),
style="bg-blue-500 p-4 rounded-lg shadow-md",
hover_style="bg-blue-600 scale-105",
active_style="scale-95"
)
The system supports a comprehensive range of utilities covering layout, spacing, typography, colors, borders, effects, transforms, and more.
You can set
any CSS property
using the
prop-[value]
syntax.
-
). If you prefer, you can also write underscores (
_
) and Dars will convert them to dashes.
[value]
, underscores (
_
) are converted to spaces. This makes it easy to write complex CSS values inside a single class token.
Examples:
style="background-image-[linear-gradient(90deg,_rgba(0,0,0,.35),_#00ffcc)]"
style="background-color-[rgba(10,20,30,.6)]"
style="padding-[calc(1rem_+_2vw)]"
style="color-[var(--brand-color)]"
style="--brand-color-[#00ffcc]"
Composable properties
Some properties are composable, meaning multiple utilities will be appended (instead of overwriting the previous value):
filter
backdrop-filter
transform
style="filter-[blur(6px)] filter-[brightness(120%)]"
This produces:
filter: blur(6px) brightness(120%);
flex
,
inline-flex
,
grid
,
inline-grid
,
block
,
inline-block
,
inline
,
table
,
table-row
,
table-cell
,
hidden
,
contents
,
flow-root
flex-row
,
flex-row-reverse
,
flex-col
,
flex-col-reverse
flex-wrap
,
flex-wrap-reverse
,
flex-nowrap
justify-start
,
justify-end
,
justify-center
,
justify-between
,
justify-around
,
justify-evenly
justify-items-start
,
justify-items-end
,
justify-items-center
,
justify-items-stretch
items-start
,
items-end
,
items-center
,
items-baseline
,
items-stretch
content-start
,
content-end
,
content-center
,
content-between
,
content-around
,
content-evenly
self-auto
,
self-start
,
self-end
,
self-center
,
self-stretch
,
self-baseline
grow
,
grow-0
,
shrink
,
shrink-0
flex-1
,
flex-auto
,
flex-initial
,
flex-none
order-{n}
basis-{value}
grid-cols-{n}
(e.g.,
grid-cols-3
,
grid-cols-12
)
grid-rows-{n}
col-span-{n}
,
col-start-{n}
,
col-end-{n}
row-span-{n}
,
row-start-{n}
,
row-end-{n}
grid-flow-row
,
grid-flow-col
,
grid-flow-dense
,
grid-flow-row-dense
,
grid-flow-col-dense
auto-cols-auto
,
auto-cols-min
,
auto-cols-max
,
auto-cols-fr
auto-rows-auto
,
auto-rows-min
,
auto-rows-max
,
auto-rows-fr
gap-{n}
,
gap-x-{n}
,
gap-y-{n}
divide-x-{n}
,
divide-y-{n}
,
divide-{color}
(adds borders between children)
p-{n}
(all sides),
px-{n}
(horizontal),
py-{n}
(vertical),
pt-{n}
(top),
pr-{n}
(right),
pb-{n}
(bottom),
pl-{n}
(left),
ps-{n}
(inline-start),
pe-{n}
(inline-end)
m-{n}
,
mx-{n}
,
my-{n}
,
mt-{n}
,
mr-{n}
,
mb-{n}
,
ml-{n}
,
ms-{n}
,
me-{n}
mx-auto
,
my-auto
0.25rem
units (e.g.,
4
=
1rem
,
8
=
2rem
)
p-[20px]
,
m-[5%]
w-{n}
,
w-full
,
w-screen
,
w-auto
,
w-min
,
w-max
,
w-fit
,
w-1/2
,
w-1/3
,
w-[300px]
h-{n}
,
h-full
,
h-screen
,
h-auto
,
h-min
,
h-max
,
h-fit
,
h-[50vh]
min-w-{n}
,
min-w-full
,
min-w-min
,
min-w-max
,
min-w-fit
min-h-{n}
,
min-h-full
,
min-h-screen
,
min-h-min
,
min-h-max
,
min-h-fit
max-w-{size}
(xs, sm, md, lg, xl, 2xl-7xl, full, min, max, fit, prose, screen-{size})
max-h-{n}
,
max-h-full
,
max-h-screen
,
max-h-min
,
max-h-max
,
max-h-fit
,
max-h-none
size-{n}
(sets both width and height)
text-xs
,
text-sm
,
text-base
,
text-lg
,
text-xl
,
text-2xl
,
text-3xl
,
text-4xl
,
text-5xl
,
text-6xl
,
text-7xl
,
text-8xl
,
text-9xl
fs-[32px]
,
fs-[2rem]
Direct font-size specification
ffam-sans
,
ffam-serif
,
ffam-mono
,
ffam-[Open_Sans]
,
ffam-[Times+New+Roman]
font-thin
,
font-extralight
,
font-light
,
font-normal
,
font-medium
,
font-semibold
,
font-bold
,
font-extrabold
,
font-black
italic
,
not-italic
text-left
,
text-center
,
text-right
,
text-justify
,
text-start
,
text-end
text-{color}-{shade}
,
text-[#123456]
underline
,
overline
,
line-through
,
no-underline
uppercase
,
lowercase
,
capitalize
,
normal-case
truncate
,
text-ellipsis
,
text-clip
align-baseline
,
align-top
,
align-middle
,
align-bottom
,
align-text-top
,
align-text-bottom
,
align-sub
,
align-super
whitespace-normal
,
whitespace-nowrap
,
whitespace-pre
,
whitespace-pre-line
,
whitespace-pre-wrap
,
whitespace-break-spaces
break-normal
,
break-words
,
break-all
,
break-keep
leading-none
,
leading-tight
,
leading-snug
,
leading-normal
,
leading-relaxed
,
leading-loose
,
leading-{n}
tracking-tighter
,
tracking-tight
,
tracking-normal
,
tracking-wide
,
tracking-wider
,
tracking-widest
indent-{n}
Smart Property Switching:
The
text-
prefix is smart. If you provide a size (e.g.,
text-xl
), it sets
font-size
. If you provide a color (e.g.,
text-white
), it automatically switches to set the
color
property.
Complete Palette (50-950 shades for each):
slate
,
gray
,
zinc
,
neutral
,
stone
red
,
rose
orange
,
amber
yellow
,
lime
green
,
emerald
,
teal
cyan
,
sky
,
blue
,
indigo
violet
,
purple
,
fuchsia
pink
black
,
white
,
transparent
,
current
Opacities:
You can set opacity for background, text, border, and rings:
-
bg-opacity-{n}
(0-100)
-
text-opacity-{n}
(0-100)
-
border-opacity-{n}
(0-100)
-
ring-opacity-{n}
(0-100)
Usage Examples:
style="bg-cyan-500 text-white" # Cyan background
style="bg-emerald-600 text-slate-50" # Emerald background with light slate text
style="bg-fuchsia-500 text-rose-100" # Fuchsia background with light rose text
style="bg-amber-400 text-zinc-900" # Amber background with dark zinc text
bg-{color}-{shade}
,
bg-[#f0f0f0]
bg-[linear-gradient(...)]
(automatically maps to
background-image
)
bg-[radial-gradient(...)]
bg-[url(...)]
bgimg-[...]
(direct
background-image
utility)
bg-bottom
,
bg-center
,
bg-left
,
bg-left-bottom
,
bg-left-top
,
bg-right
,
bg-right-bottom
,
bg-right-top
,
bg-top
bg-repeat
,
bg-no-repeat
,
bg-repeat-x
,
bg-repeat-y
,
bg-repeat-round
,
bg-repeat-space
bg-auto
,
bg-cover
,
bg-contain
bg-fixed
,
bg-local
,
bg-scroll
bg-clip-border
,
bg-clip-padding
,
bg-clip-content
,
bg-clip-text
bg-origin-border
,
bg-origin-padding
,
bg-origin-content
bg-gradient-to-{t|tr|r|br|b|bl|l|tl}
: Sets the gradient direction.
from-{color}
: Sets the start color.
via-{color}
: Sets an intermediate color.
to-{color}
: Sets the end color.
Gradient Example:
style="bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500"
Dars uses a modern CSS variable architecture for gradients (
--tw-gradient-stops
), ensuring they are performant and easy to manipulate.
-
Background Blend Mode
:
bg-blend-normal
,
bg-blend-multiply
,
bg-blend-screen
,
bg-blend-overlay
,
bg-blend-darken
,
bg-blend-lighten
,
bg-blend-color-dodge
,
bg-blend-color-burn
,
bg-blend-hard-light
,
bg-blend-soft-light
,
bg-blend-difference
,
bg-blend-exclusion
,
bg-blend-hue
,
bg-blend-saturation
,
bg-blend-color
,
bg-blend-luminosity
border
,
border-0
,
border-2
,
border-4
,
border-8
border-t-{n}
,
border-r-{n}
,
border-b-{n}
,
border-l-{n}
,
border-x-{n}
,
border-y-{n}
border-{color}-{shade}
,
border-[#123456]
border-t-{color}
,
border-r-{color}
,
border-b-{color}
,
border-l-{color}
,
border-x-{color}
,
border-y-{color}
border-solid
,
border-dashed
,
border-dotted
,
border-double
,
border-hidden
,
border-none
rounded
,
rounded-none
,
rounded-sm
,
rounded-md
,
rounded-lg
,
rounded-xl
,
rounded-2xl
,
rounded-3xl
,
rounded-full
,
rounded-[10px]
rounded-t-{size}
,
rounded-r-{size}
,
rounded-b-{size}
,
rounded-l-{size}
,
rounded-tl-{size}
,
rounded-tr-{size}
,
rounded-br-{size}
,
rounded-bl-{size}
shadow-sm
,
shadow
,
shadow-md
,
shadow-lg
,
shadow-xl
,
shadow-2xl
,
shadow-inner
,
shadow-none
opacity-0
,
opacity-25
,
opacity-50
,
opacity-75
,
opacity-100
ring
,
ring-{n}
: Adds an outer ring (box-shadow) with a specific width (e.g.,
ring-4
).
ring-{color}
: Sets the color of the ring (e.g.,
ring-blue-500
).
ring-offset-{n}
: Adds a white offset between the element and the ring.
ring-opacity-{n}
: Sets the transparency of the ring.
Ring Example:
style="ring-4 ring-indigo-500 ring-opacity-50 ring-offset-2"
mix-blend-normal
,
mix-blend-multiply
,
mix-blend-screen
,
mix-blend-overlay
,
mix-blend-darken
,
mix-blend-lighten
,
mix-blend-color-dodge
,
mix-blend-color-burn
,
mix-blend-hard-light
,
mix-blend-soft-light
,
mix-blend-difference
,
mix-blend-exclusion
,
mix-blend-hue
,
mix-blend-saturation
,
mix-blend-color
,
mix-blend-luminosity
blur-none
,
blur-sm
,
blur
,
blur-md
,
blur-lg
,
blur-xl
,
blur-2xl
,
blur-3xl
brightness-{n}
(0-200)
contrast-{n}
(0-200)
grayscale-{n}
(0-100)
hue-rotate-{n}
(degrees)
invert-{n}
(0-100)
saturate-{n}
(0-200)
sepia-{n}
(0-100)
drop-shadow-sm
,
drop-shadow
,
drop-shadow-md
,
drop-shadow-lg
,
drop-shadow-xl
,
drop-shadow-2xl
,
drop-shadow-none
backdrop-blur-{size}
backdrop-brightness-{n}
backdrop-contrast-{n}
backdrop-grayscale-{n}
backdrop-hue-rotate-{n}
backdrop-invert-{n}
backdrop-opacity-{n}
backdrop-saturate-{n}
backdrop-sepia-{n}
scale-{n}
,
scale-x-{n}
,
scale-y-{n}
(e.g.,
scale-110
= 110%)
rotate-{n}
(degrees)
translate-x-{n}
,
translate-y-{n}
skew-x-{n}
,
skew-y-{n}
(degrees)
origin-center
,
origin-top
,
origin-top-right
,
origin-right
,
origin-bottom-right
,
origin-bottom
,
origin-bottom-left
,
origin-left
,
origin-top-left
transition-none
,
transition-all
,
transition-colors
,
transition-opacity
,
transition-shadow
,
transition-transform
duration-{ms}
(e.g.,
duration-300
,
duration-500
)
delay-{ms}
ease-linear
,
ease-in
,
ease-out
,
ease-in-out
static
,
fixed
,
absolute
,
relative
,
sticky
top-{n}
,
right-{n}
,
bottom-{n}
,
left-{n}
inset-{n}
,
inset-x-{n}
,
inset-y-{n}
z-{n}
(e.g.,
z-10
,
z-50
)
outline-{n}
,
outline-{color}
,
outline-offset-{n}
,
outline-dashed
,
outline-dotted
accent-{color}
,
caret-{color}
overflow-auto
,
overflow-hidden
,
overflow-clip
,
overflow-visible
,
overflow-scroll
overflow-x-auto
,
overflow-x-hidden
,
overflow-x-clip
,
overflow-x-visible
,
overflow-x-scroll
overflow-y-auto
,
overflow-y-hidden
,
overflow-y-clip
,
overflow-y-visible
,
overflow-y-scroll
visible
,
invisible
,
collapse
cursor-auto
,
cursor-default
,
cursor-pointer
,
cursor-wait
,
cursor-text
,
cursor-move
,
cursor-help
,
cursor-not-allowed
,
cursor-none
,
cursor-context-menu
,
cursor-progress
,
cursor-cell
,
cursor-crosshair
,
cursor-vertical-text
,
cursor-alias
,
cursor-copy
,
cursor-no-drop
,
cursor-grab
,
cursor-grabbing
,
cursor-all-scroll
,
cursor-col-resize
,
cursor-row-resize
,
cursor-n-resize
,
cursor-e-resize
,
cursor-s-resize
,
cursor-w-resize
,
cursor-ne-resize
,
cursor-nw-resize
,
cursor-se-resize
,
cursor-sw-resize
,
cursor-ew-resize
,
cursor-ns-resize
,
cursor-nesw-resize
,
cursor-nwse-resize
,
cursor-zoom-in
,
cursor-zoom-out
pointer-events-none
,
pointer-events-auto
select-none
,
select-text
,
select-all
,
select-auto
object-contain
,
object-cover
,
object-fill
,
object-none
,
object-scale-down
object-bottom
,
object-center
,
object-left
,
object-left-bottom
,
object-left-top
,
object-right
,
object-right-bottom
,
object-right-top
,
object-top
aspect-auto
,
aspect-square
,
aspect-video
,
aspect-{w}-{h}
(e.g.,
aspect-16-9
)
float-right
,
float-left
,
float-none
clear-left
,
clear-right
,
clear-both
,
clear-none
box-border
,
box-content
isolate
,
isolation-auto
list-none
,
list-disc
,
list-decimal
list-inside
,
list-outside
appearance-none
,
appearance-auto
resize-none
,
resize-y
,
resize-x
,
resize
scroll-auto
,
scroll-smooth
snap-start
,
snap-end
,
snap-center
,
snap-align-none
snap-normal
,
snap-always
snap-none
,
snap-x
,
snap-y
,
snap-both
,
snap-mandatory
,
snap-proximity
scroll-m-{n}
,
scroll-mx-{n}
,
scroll-my-{n}
,
scroll-mt-{n}
,
scroll-mr-{n}
,
scroll-mb-{n}
,
scroll-ml-{n}
scroll-p-{n}
,
scroll-px-{n}
,
scroll-py-{n}
,
scroll-pt-{n}
,
scroll-pr-{n}
,
scroll-pb-{n}
,
scroll-pl-{n}
touch-auto
,
touch-none
,
touch-pan-x
,
touch-pan-left
,
touch-pan-right
,
touch-pan-y
,
touch-pan-up
,
touch-pan-down
,
touch-pinch-zoom
,
touch-manipulation
will-change-auto
,
will-change-scroll
,
will-change-contents
,
will-change-transform
columns-{n}
break-after-{value}
break-before-{value}
break-inside-{value}
For values not covered by the standard scale, use square brackets
[]
:
Container(
style="w-[350px] bg-[#1a2b3c] z-[100] top-[50px] fs-[24px]"
)
Arbitrary value features:
- Use underscores for spaces:
bg-[url('image.jpg')]
→
bg-[url('image.jpg')]
- Works with any property:
p-[20px]
,
m-[5%]
,
w-[calc(100%-50px)]
You can define styles for specific states using
hover_style
and
active_style
arguments.
Button(
"Click Me",
style="bg-blue-500 text-white px-4 py-2 rounded transition-all duration-300",
hover_style="bg-blue-600 shadow-lg scale-105",
active_style="bg-blue-700 scale-95"
)
from dars.all import *
app = App("Styling Demo")
@route("/")
def index():
return Page(
Container(
# Header with gradient text
Text(
"Welcome to Dars",
style="fs-[48px] font-black text-center mb-8"
),
# Card with new colors
Container(
Text("Cyan Card", style="text-xl font-bold mb-2"),
Text("Using the new cyan color palette", style="text-cyan-100"),
style="bg-cyan-600 p-6 rounded-xl shadow-xl mb-4"
),
Container(
Text("Emerald Card", style="text-xl font-bold mb-2"),
Text("With emerald green background", style="text-emerald-100"),
style="bg-emerald-600 p-6 rounded-xl shadow-xl mb-4"
),
# Interactive button
Button(
"Hover Me",
style="bg-fuchsia-500 text-white px-6 py-3 rounded-lg transition-all duration-300",
hover_style="bg-fuchsia-600 scale-110 shadow-2xl",
active_style="scale-95"
),
style="max-w-4xl mx-auto p-8"
),
style="min-h-screen bg-gradient-to-br from-slate-50 to-zinc-100"
)
app.add_page("index", index())
if __name__ == "__main__":
app.rTimeCompile()
The parsing happens in Python before the HTML/CSS is generated. This means:
0.25rem
scale (4, 8, 12, 16, etc.) for consistency
transition-all duration-300
for smooth hover effects
style="""
bg-gradient-to-r from-purple-600 to-pink-600
text-white px-8 py-4 rounded-xl shadow-2xl
transition-all duration-300
"""
For animations, see the Animation System documentation.
You can define your own utility classes in
dars.config.json
under the
utility_styles
key. This allows you to create reusable style combinations, use raw CSS properties, and even compose other custom utilities.
Configuration (
dars.config.json
):
{
"utility_styles": {
"btn-primary": [
"bg-blue-600",
"text-white",
"p-3",
"rounded-lg",
"hover:bg-blue-700",
"transition-all"
],
"card-fancy": [
"bg-white",
"p-8",
"rounded-xl",
"shadow-lg",
"border: 1px solid #e5e7eb" // Raw CSS property
],
"text-gradient": [
"font-bold",
"text-4xl",
"background: linear-gradient(to right, #4f46e5, #ec4899)",
"-webkit-background-clip: text",
"-webkit-text-fill-color: transparent",
"display: inline-block"
]
}
}
Usage in Python:
Button(text="Click Me", style="btn-primary")
Container(style="card-fancy text-gradient")
Features:
-
Composition
: Combine multiple existing utilities into one class.
-
Raw CSS
: Use standard CSS syntax (e.g.,
border: 1px solid red
) directly in the list.
Dars offers two ways to create custom components: Function Components (Recommended) and Class Components (Legacy).
Function Components are the modern way to create reusable UI elements. They use simple functions with f-string templates and automatically handle framework features like IDs, styling, and events.
Use the
@FunctionComponent
decorator. You can access framework properties (
id
,
class_name
,
style
,
children
) using the
Props
helper object or by declaring them as arguments.
Props
Object (Cleanest)
from dars.all import *
@FunctionComponent
def UserCard(name, email, **props):
return f"""
<div {Props.id} {Props.class_name} {Props.style}>
<h3>{name}</h3>
<p>{email}</p>
<div class="card-body">
{Props.children}
</div>
</div>
"""
# Usage
card = UserCard("John Doe", "john@example.com", id="user-1", style="p-5")
@FunctionComponent
def UserCard(name, email, id, class_name, style, children, **props):
return f"""
<div {id} {class_name} {style}>
<h3>{name}</h3>
<p>{email}</p>
<div class="card-body">
{children}
</div>
</div>
"""
{id}
,
{class_name}
, and
{style}
.
State()
and reactive updates.
on_click
are handled automatically by the framework (passed via
**props
).
{Props.children}
or
{children}
to render nested content.
@FunctionComponent
def Counter(**props):
return f"""
<div {Props.id} {Props.class_name} {Props.style}>
0 {Props.children}
</div>
"""
# Create component with initial value "0"
counter = Counter(id="my-counter", children="0")
# Make it reactive controlling the 'text' property (textContent)
# Note: This replaces the entire content of the div with the new text
state = State(counter, text="0")
# Update it
Button("Increment", on_click=state.text.set("5"))
FunctionComponents work seamlessly with all Dars hooks, enabling reactive and interactive behavior.
Use
useDynamic()
to create reactive text that updates automatically when state changes:
from dars.all import *
userState = State("user", name="John Doe", status="Active")
@FunctionComponent
def UserCard(**props):
return f'''
<div {Props.id} {Props.class_name} {Props.style}>
<h3>Name: {useDynamic("user.name")}</h3>
<p>Status: {useDynamic("user.status")}</p>
{Props.children}
</div>
'''
# The name and status will update automatically when state changes
card = UserCard(id="user-card")
Button("Update", on_click=userState.name.set("Jane Doe"))
Use
useValue()
with selectors to set initial values and enable value extraction:
from dars.all import *
app = App("Example of hooks")
userState = State("user", name="Jane Doe", email="jane@example.com", display="None")
@FunctionComponent
def UserForm(**props):
return f'''
<div {Props.id} {Props.class_name} {Props.style}>
<input value="{useValue("user.name", ".name-input")}" />
<input value="{useValue("user.email", "#email-field")}" />
<span>{useValue("user.age", ".age-display")}</span>
<span>{useDynamic("user.display")}</span>
</div>
'''
@route("/")
def index():
return Page(
UserForm(id="user-form"),
# Extract values using V() helper with the selectors
Button(
"Get Name",
on_click=userState.display.set(
"Name: " + V(".name-input") # Extract current value
)
),
Button(
"Combine Values",
on_click=userState.display.set(
V(".name-input") + " (" + V("#email-field") + ")"
)
)
)
app.add_page("index", index(), title="hooks", index=True)
if __name__ == "__main__":
app.rTimeCompile()
Use
useWatch()
to monitor state changes and execute side effects:
from dars.all import *
app = App("Example of hooks")
cartState = State("cart", total=0.0)
@FunctionComponent
def CartSummary(total=0,**props):
return f'''
<div {Props.id} {Props.class_name} {Props.style}>
<h3>Cart Total: ${useDynamic("cart.total")}</h3>
{Props.children}
</div>
'''
# Watch for cart changes and log
app.useWatch("cart.total", log("Cart total changed!"))
@route("/")
def index():
return Page(
CartSummary(id="cart-summary", total=0),
# Button to add $10 to cart total using V() with state path
Button("Add $10", on_click=cartState.total.set(
V("cart.total").float() + 10
))
)
app.add_page("index", index(), title="hooks", index=True)
if __name__ == "__main__":
app.rTimeCompile()
You can combine multiple hooks for complex interactive components:
from dars.all import *
app = App("Example of hooks")
productState = State("product",
name="Widget",
price=19.99,
quantity=1,
total=19.99
)
@FunctionComponent
def ProductCard(**props):
return f'''
<div {Props.id} {Props.class_name} {Props.style}>
<!-- useDynamic for reactive display -->
<h3>{useDynamic("product.name")}</h3>
<p>Price: ${useDynamic("product.price")}</p>
<!-- useValue for editable quantity -->
<h3>Number to multiply with price</h3>
<input type="number"
value="{useValue("product.quantity", ".qty-input")}"
min="1" />
<!-- useDynamic for calculated total -->
<p>Total: ${useDynamic("product.total")}</p>
{Props.children}
</div>
'''
# Watch for total changes and show alert
app.useWatch("product.total", log("Total updated!"))
@route("/")
def index():
return Page(
ProductCard(id="product-card",name="Milk", price=100, quantity=0, total=0 ),
Button(
"Calculate Total",
on_click=productState.total.set(
V(".qty-input").int() * V("product.price").float()
)
)
)
app.add_page("index", index(), title="hooks", index=True)
if __name__ == "__main__":
app.rTimeCompile()
This is the older method of creating components by inheriting from the
Component
class. It is more verbose and requires manual handling of rendering logic.
from dars.all import *
from dars.core.component import Component
class CustomComponent(Component):
def __init__(self, title: str, id: str = None, **props):
super().__init__(**props)
self.title = title
self.id = id
# Manual event attachment
self.set_event(EventTypes.CLICK, log('click'))
def render(self, exporter: 'Exporter') -> str:
# Manual children rendering
children_html = self.render_children(exporter)
return f'''
<div class="my-component" id="{self.id}" style="{self.style}">
<h2>{self.title}</h2>
<div class="content">
{children_html}
</div>
</div>
'''
Dars provides a powerful and easy-to-use animation system built on top of the Web Animations API. It allows you to add professional-grade animations to your components with simple Python function calls.
All animations in Dars are
dScript
objects. This means they:
- Run entirely on the client side (zero latency)
- Can be assigned to any event handler (
on_click
,
on_mouseover
, etc.)
- Can be chained together using
.then()
or the
sequence()
helper
- Return Promises, allowing for complex orchestration
from dars.all import *
# Simple fade in
button.on_click = fadeIn(id="my-element")
# Chain animations
button.on_click = sequence(
fadeOut(id="old-panel"),
fadeIn(id="new-panel")
)
Control visibility with opacity transitions.
fadeIn(id, duration=300, easing="ease")
Fades an element in from opacity 0 to 1. Sets
display: block
automatically.
fadeIn(id="modal", duration=500)
fadeOut(id, duration=300, easing="ease", hide=True)
Fades an element out from current opacity to 0.
-
hide
: If
True
(default), sets
display: none
after animation completes.
fadeOut(id="notification", duration=2000, hide=True)
Move elements into or out of view.
slideIn(id, direction="down", duration=300, easing="ease")
Slides an element into its final position.
-
direction
:
"up"
,
"down"
,
"left"
,
"right"
(from where it enters)
slideIn(id="sidebar", direction="left", duration=400)
slideOut(id, direction="up", duration=300, easing="ease", hide=True)
Slides an element out of view.
-
direction
:
"up"
,
"down"
,
"left"
,
"right"
(to where it exits)
slideOut(id="sidebar", direction="left", duration=400)
Zoom elements in and out.
scaleIn(id, duration=300, easing="ease", from_scale=0.0)
Scales an element up to its natural size (scale 1).
-
from_scale
: Starting scale factor (0.0 to 1.0)
scaleIn(id="popup", from_scale=0.5)
scaleOut(id, duration=300, easing="ease", to_scale=0.0, hide=True)
Scales an element down.
-
to_scale
: Ending scale factor (0.0 to 1.0)
scaleOut(id="popup", to_scale=0.0)
Draw user attention to elements.
shake(id, intensity=5, duration=500)
Shakes an element horizontally. Great for error feedback.
-
intensity
: Shake distance in pixels.
shake(id="login-form", intensity=10)
bounce(id, distance=20, duration=600)
Bounces an element vertically.
-
distance
: Bounce height in pixels.
bounce(id="notification-icon", distance=15)
pulse(id, scale=1.1, duration=400, iterations=1)
Pulses an element (scales up and down).
-
scale
: Max scale during pulse.
-
iterations
: Number of pulses. Use
"infinite"
for continuous pulsing.
# Single pulse
pulse(id="heart-icon")
# Continuous heartbeat
pulse(id="status-dot", iterations="infinite", duration=1000)
Rotate and flip elements.
rotate(id, degrees=360, duration=500, easing="ease")
Rotates an element.
rotate(id="refresh-icon", degrees=180)
flip(id, axis="y", duration=600)
Flips an element 180 degrees around an axis.
-
axis
:
"x"
(horizontal flip) or
"y"
(vertical flip).
flip(id="card", axis="y")
Animate specific CSS properties.
colorChange(id, from_color, to_color, duration=500, property="background-color")
Smoothly transitions a color property.
colorChange(id="btn", from_color="#fff", to_color="#f00", property="background-color")
morphSize(id, to_width, to_height, duration=500, easing="ease")
Changes the dimensions of an element.
morphSize(id="panel", to_width="100%", to_height="500px")
You can run animations in sequence using the
sequence()
helper or the
.then()
method.
sequence()
The easiest way to run animations one after another.
from dars.all import sequence, fadeIn, slideIn
button.on_click = sequence(
fadeIn(id="header"),
slideIn(id="content", direction="up"),
fadeIn(id="footer")
)
.then()
For more granular control or branching logic.
anim1 = fadeIn(id="box1")
anim2 = slideIn(id="box2")
# Run anim1, then anim2
button.on_click = anim1.then(anim2)
To run animations simultaneously, simply trigger them in the same event handler (or use a list of handlers).
# Both start at the same time
button.on_click = [
fadeIn(id="box1"),
slideIn(id="box2")
]
Dars Framework 1.4.5 introduces a powerful SPA (Single Page Application) routing system that supports nested routes, layouts, and automatic 404 handling.
To create a basic SPA, you define pages and add them to your app. One page must be designated as the index.
from dars.all import *
app = App(title="My SPA App")
# Create pages
home = Page(Container(Text("Home Page")))
about = Page(Container(Text("About Us")))
# Add pages to app
app.add_page(name="home", root=home, route="/", title="Home", index=True)
app.add_page(name="about", root=about, route="/about", title="About")
you can also use the
@route
decorator to add pages to your app.
from dars.all import *
app = App(title="My SPA App")
# Create pages
home = Page(Container(Text("Home Page")))
about = Page(Container(Text("About Us")))
# Add pages to app
@app.route("/")
def home():
return Page(Container(Text("Home Page")))
@app.route("/about")
def about():
return Page(Container(Text("About Us")))
app.add_page("home", home, title="Home", index=True)
app.add_page("about", about, title="About")
Note: If you use the
@routedecorator, you can't add the route of the page inapp.add_page().
Nested routes allow you to create layouts that persist while child content changes. This is achieved using the
parent
parameter and the
Outlet
component.
The
Outlet
component serves as a placeholder where child routes will be rendered within a parent layout.
from dars.components.advanced.outlet import Outlet
# Parent Layout (Dashboard)
dashboard_layout = Page(
Container(
Text("Dashboard Header"),
# Child routes will render here:
Outlet(),
Text("Dashboard Footer")
)
)
# Child Page (Settings)
settings_page = Page(
Container(Text("Settings Content"))
)
The
Outlet
can also render an optional placeholder while the child route is still loading (SSR lazy-load or SPA navigation).
If
placeholder
is not provided, nothing is rendered.
from dars.components.advanced.outlet import Outlet
dashboard_layout = Page(
Container(
Text("Dashboard Header"),
Outlet(
placeholder=Container(Text("Loading section..."))
),
Text("Dashboard Footer")
)
)
You can declare multiple outlets in the same layout by giving each
Outlet
an
outlet_id
.
Child routes can then target a specific outlet via
app.add_page(..., outlet_id="...")
.
from dars.components.advanced.outlet import Outlet
dashboard_layout = Page(
Container(
Text("Dashboard Header"),
Container(
Outlet(outlet_id="main"),
Outlet(outlet_id="sidebar", placeholder=Text("Loading sidebar...")),
style="flex gap-4"
),
Text("Dashboard Footer")
)
)
Use the
parent
parameter in
add_page
to define the hierarchy.
# 1. Add the parent route
app.add_page(
name="dashboard",
root=dashboard_layout,
route="/dashboard",
title="Dashboard"
)
# 2. Add the child route, specifying the parent's name
app.add_page(
name="settings",
root=settings_page,
route="/dashboard/settings",
title="Settings",
parent="dashboard" # This links it to the dashboard layout
)
If your parent layout contains multiple outlets, pass
outlet_id
in the child route to target the correct outlet:
app.add_page(
name="dashboard",
root=dashboard_layout,
route="/dashboard",
title="Dashboard"
)
app.add_page(
name="settings",
root=settings_page,
route="/dashboard/settings",
title="Settings",
parent="dashboard",
outlet_id="main"
)
When you navigate to
/dashboard/settings
, Dars will render the
dashboard
layout and place the
settings
content inside the
Outlet
.
The SPA router normalizes paths so that trailing slashes do not create false 404s:
/dashboard
and
/dashboard/
are treated as the same route.
/
remains
/
.
Dars provides robust handling for non-existent routes.
If a user navigates to a route that doesn't exist, Dars automatically:
1. Redirects the user to
/404
.
2. Displays a built-in, clean "404 Page Not Found" error page.
You can customize the 404 page using
app.set_404_page()
.
# Create your custom 404 page
not_found_page = Page(
Container(
Text("Oops! Page not found 😢", style="text-3xl"),
Link("Go Home", href="/")
)
)
# Register it
app.set_404_page(not_found_page)
Now, when a 404 occurs, users will be redirected to
/404
but will see your custom design.
Similar to 404 pages, you can define a custom 403 Forbidden page for unauthorized access to private routes.
Dars includes a default 403 page that informs users they don't have permission to access the requested resource.
You can customize the 403 page using
app.set_403_page()
.
# Create your custom 403 page
forbidden_page = Page(
Container(
Text("⛔ Access Denied", style="text-3xl text-red-500"),
Text("You do not have permission to view this page."),
Link("Go to Login", href="/login")
)
)
# Register it
app.set_403_page(forbidden_page)
Dars will automatically redirect to
/prohibited
and show this page when a user tries to access a private route without authentication.
The development server (
dars dev
) includes an intelligent hot reload system for SPAs:
Dars handles SEO automatically in Single Page Applications. The router intelligently updates the document metadata when navigating between routes.
To control page metadata for each route, use the
Head
component:
from dars.components.advanced.head import Head
@app.route("/about")
def about():
return Page(
Head(
title="About Us - My App",
description="Learn more about our company.",
og_image="/images/about-og.jpg"
),
Container(Text("About Content"))
)
The router dynamically updates:
-
<title>
- Meta tags (
description
,
keywords
, etc.)
- Open Graph tags (
og:title
,
og:type
, etc.)
- Twitter Cards
This ensures that even client-side routes display the correct information in the browser tab and when shared on social media.
Dars Framework provides complete Server-Side Rendering support integrated with FastAPI, allowing you to build full-stack applications with both server-rendered and client-side pages.
SSR routes are rendered on the server before being sent to the client, providing: - Faster initial page load - Progressive enhancement - Flexible architecture (mix SSR, SPA, and Static routes)
from dars.all import *
from backend.apiConfig import DarsEnv
# Configure SSR URL
ssr_url = DarsEnv.get_urls()['backend']
app = App(title="My App", ssr_url=ssr_url)
# Define SSR route
@route("/", route_type=RouteType.SSR)
def home():
return Page(
Text("Welcome!", style="text-3xl font-bold"),
Text("This page is rendered on the server!")
)
app.add_page("home", home(), title="Home")
Dars uses a sophisticated "Dual Hydration" approach:
This prevents Flash of Unstyled Content (FOUC), double rendering, and race conditions.
Use the Dars CLI to scaffold a complete SSR project with FastAPI backend:
dars init my-ssr-app --type ssr
cd my-ssr-app
This creates a full-stack project with:
- Frontend Dars app (
main.py
)
- FastAPI backend (
backend/api.py
)
- Environment configuration
- Development and production setup
For comprehensive SSR documentation including:
- Architecture and how it works
- Development workflow
- API reference (
create_ssr_app
,
SSRRenderer
)
- Mixing SSR, SPA, and Static routes
- Deployment guide
- Advanced features (authentication, custom endpoints)
- Best practices and troubleshooting
- Real-world examples
See the Complete SSR Guide
Dars Framework provides a complete Server-Side Rendering solution integrated with FastAPI, allowing you to build full-stack applications with both server-rendered and client-side pages in a single codebase.
SSR in Dars renders your pages on the server before sending them to the client, providing:
/dashboard
)
Dars uses a sophisticated "Dual Hydration" approach to prevent flickering and ensure smooth transitions:
Server Side:
1. Render component to HTML
2. Build VDOM representation
3. Inject VDOM as window.__ROUTE_VDOM__
4. Send HTML + VDOM to client
Client Side:
1. Display server-rendered HTML (instant)
2. Load dars.min.js runtime
3. Detect __ROUTE_VDOM__ presence
4. Hydrate DOM without re-rendering
5. Attach event handlers and state
This prevents: - Flash of Unstyled Content (FOUC) - Double rendering - Race conditions - Lost event handlers
SSR routes are fully SEO-optimized. Using the
Head
component allows you to inject metadata directly into the server-rendered HTML.
@route("/blog/post-1", route_type=RouteType.SSR)
def blog_post():
return Page(
Head(
title="My Amazing Blog Post",
description="Read this incredible story...",
keywords="blog, story, amazing",
og_type="article"
),
# ... content ...
)
Use the Dars CLI to scaffold a complete SSR project:
dars init my-ssr-app --type ssr
cd my-ssr-app
This creates:
my-ssr-app/
├── main.py # Frontend (Dars app)
├── backend/
│ ├── api.py # FastAPI server with SSR
│ └── apiConfig.py # Environment configuration
└── dars.config.json # Dars configuration
main.py
)
from dars.all import *
from backend.apiConfig import DarsEnv
# Configure SSR URL
ssr_url = DarsEnv.get_urls()['backend']
app = App(title="My SSR App", ssr_url=ssr_url)
# Define SSR route
@route("/", route_type=RouteType.SSR)
def index():
return Page(
Text("Hello from Server!", style="fs-[32px]"),
Button("Click Me", on_click=alert("Interactive!"))
)
app.add_page("index", index(), title="Home")
if __name__ == "__main__":
app.rTimeCompile()
backend/api.py
)
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from dars.backend.ssr import create_ssr_app
from apiConfig import DarsEnv
# Import Dars app
import sys
sys.path.insert(0, '.')
from main import app as dars_app
# Create FastAPI app with SSR
app = create_ssr_app(dars_app)
# Enable CORS for development
if DarsEnv.is_dev():
urls = DarsEnv.get_urls()
app.add_middleware(
CORSMiddleware,
allow_origins=[urls['frontend']],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=3000)
backend/apiConfig.py
)
class DarsEnv:
MODE = "development" # or "production"
DEV = "development"
BUILD = "production"
@staticmethod
def is_dev():
return DarsEnv.MODE == DarsEnv.DEV
@staticmethod
def get_urls():
if DarsEnv.is_dev():
return {
"backend": "http://localhost:3000",
"frontend": "http://localhost:8000"
}
return {
"backend": "/",
"frontend": "/"
}
Dars supports two routing modes that can be mixed in the same application:
RouteType.SSR
)
Server-rendered on every request.
@route("/dashboard", route_type=RouteType.SSR)
def dashboard():
return Page(
Text("Dashboard", style="text-3xl font-bold"),
Text(f"Rendered at: {datetime.now()}")
)
When to use: - SEO-critical pages (landing pages, blog posts) - Dynamic content that changes frequently - Pages requiring authentication checks server-side - Initial load performance is critical
RouteType.PUBLIC
) - Default
Client-side navigation, no server rendering.
@route("/settings") # Default is PUBLIC
def settings():
return Page(
Text("Settings", level=1),
# Interactive forms, real-time updates
)
When to use: - Admin dashboards (not recommended for now in the future will be supported with Dars Middleware) - Interactive tools - Pages behind authentication - Real-time applications
When you navigate to an
RouteType.SSR
route from the SPA router, the client fetches route data from the backend (
/api/ssr/...
).
You can configure global loading and error placeholders for this lazy-load step:
app.set_loading_state(
loadingComp=Page(Container(Text("Loading..."))),
onErrorComp=Page(Container(Text("Failed to load route")))
)
These placeholders are rendered as static HTML (similar to SPA 404/403 pages), which means they do not register states/events and do not interfere with hydration.
Outlet(placeholder=...)
For nested routes, layouts typically include one or more
Outlet
placeholders. You can optionally render a layout-level placeholder inside an outlet:
Outlet(outlet_id="main", placeholder=Container(Text("Loading section...")))
This is useful when the parent layout is already visible and you want a placeholder only for the child region.
You need two processes running simultaneously:
Terminal 1 - Frontend Dev Server:
dars dev
# Runs on http://localhost:8000
# Uses app.rTimeCompile() with hot reload for UI changes
Terminal 2 - Backend SSR Server:
dars dev --backend
# Runs on http://localhost:3000
# Starts uvicorn with the backendEntry from dars.config.json (by default "backend.api:app")
Backend Server (3000) : Renders SSR routes and provides API endpoints.
Communication
: Frontend fetches SSR content from backend via
/api/ssr/*
The
DarsEnv
class automatically configures URLs:
# Development
DarsEnv.get_urls() → {
"backend": "http://localhost:3000",
"frontend": "http://localhost:8000"
}
# Production
DarsEnv.get_urls() → {
"backend": "/",
"frontend": "/"
}
create_ssr_app(dars_app, prefix="/api/ssr", streaming=False)
Creates a FastAPI application with automatic SSR endpoints.
Parameters:
-
dars_app
(App): Your Dars application instance
-
prefix
(str): URL prefix for SSR endpoints (default:
/api/ssr
)
-
streaming
(bool): When
True
, enables HTML streaming so the
<head>
and opening
<body>
are sent first, and the rest of the document is streamed afterwards. Default is
False
(classic non-streaming response).
Returns: - FastAPI application with registered SSR routes
Auto-generated Endpoints:
For each SSR route in your Dars app,
create_ssr_app
creates:
-
GET {prefix}/{route_name}
- JSON payload used by the SPA router for lazy SSR loading
-
GET {route_path}
- Full HTML SSR endpoint (e.g.
/
,
/blog
,
/dashboard
)
Additionally, if no SSR route takes
/
, a health-check endpoint is added at:
-
GET /
- Returns basic JSON info about the SSR backend
Example (non-streaming):
from dars.backend.ssr import create_ssr_app
app = create_ssr_app(dars_app)
# Automatically creates, for example:
# - GET /api/ssr/index
# - GET /api/ssr/dashboard
# - GET / (if root not taken by an SSR route)
Example (streaming enabled):
from dars.backend.ssr import create_ssr_app
app = create_ssr_app(dars_app, streaming=True)
# HTML responses for SSR routes are sent in two chunks:
# 1) <html> + <head> + opening <body>
# 2) The rest of the document (body content + scripts)
Behind the scenes,
create_ssr_app
uses
SSRRenderer
to:
- Render the SSR route to HTML and wrap it in
__dars_spa_root__
for hydration.
- Build a minimal SPA config and expose it as
window.__DARS_SPA_CONFIG__
.
- Serialize initial state snapshots:
- V1:
window.__DARS_STATE__
(STATE_BOOTSTRAP)
- V2:
window.__DARS_STATE_V2__
(STATE_V2_REGISTRY via
to_dict()
)
- Inject a VDOM snapshot as
window.__ROUTE_VDOM__
.
SSRRenderer
Low-level class for manual SSR rendering.
from dars.backend.ssr import SSRRenderer
renderer = SSRRenderer(dars_app)
result = renderer.render_route("dashboard", params={"user_id": "123"})
# Returns (simplified):
{
"name": "dashboard",
"html": "<div>...</div>", # Body HTML for SPA hydration
"fullHtml": "<!DOCTYPE html>...", # Complete HTML document with <head>
"scripts": [...], # Core + page-specific scripts
"events": {...}, # Event map for client-side binding
"vdom": {...}, # VDOM snapshot for the route
"states": [...], # V1 state snapshot (STATE_BOOTSTRAP)
"statesV2": [...], # V2 state snapshot (STATE_V2_REGISTRY)
"spaConfig": {...}, # Minimal SPA routing config
"headMetadata": {...} # Metadata extracted from Head component
}
app = App(title="Hybrid App", ssr_url=ssr_url)
# SSR for landing page (SEO)
@route("/", route_type=RouteType.SSR)
def home():
return Page(Text("Welcome!"))
# SPA for dashboard (interactive)
@route("/dashboard")
def dashboard():
return Page(Text("Dashboard"))
# Static for docs (performance)
@route("/docs", route_type=RouteType.STATIC)
def docs():
return Page(Text("Documentation"))
app.add_page("home", home(), title="Home", index=True)
app.add_page("dashboard", dashboard(), title="Dashboard")
app.add_page("docs", docs(), title="Docs")
Navigation Behavior: - SSR → SPA: Fetches from backend, hydrates - SPA → SPA: Client-side navigation (instant) - Any → Static: Loads pre-rendered HTML
1. Update Environment Mode:
# backend/apiConfig.py
class DarsEnv:
MODE = "production" # Change from "development"
2. Build Frontend:
dars build
# Generates static files in ./dist
3. Deploy Backend:
Your FastAPI backend serves both:
- SSR-rendered pages via
/api/ssr/*
- Static files from
./dist
Example Production Server:
# backend/api.py
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from dars.backend.ssr import create_ssr_app
from main import app as dars_app
app = create_ssr_app(dars_app)
# Serve static files
app.mount("/", StaticFiles(directory="dist", html=True), name="static")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Vercel / Netlify: - Deploy FastAPI backend as serverless function - Serve static files from CDN - Configure environment variables
Dars Framework features powerful state management systems , designed for different use cases:
Modern, Pythonic state management for reactive UIs.
from dars.all import *
# Create a component
display = Text("0", id="counter")
# Create state
counter = State(display, text=0)
# Use reactive properties
increment_btn = Button("Increment", on_click=counter.text.increment(by=1))
decrement_btn = Button("Decrement", on_click=counter.text.decrement(by=1))
reset_btn = Button("Reset", on_click=counter.reset())
The
State
class wraps a component and provides reactive property access.
from dars.all import State
display = Text("0", id="counter")
counter_state = State(display, text=0)
Constructor Parameters:
-
component
: The component to manage (can be a component object or string ID)
-
**default_props
: Default property values (e.g.,
text=0
,
style="..."
)
State()
can accept either a component object or a string ID. This is useful for components created dynamically:
from dars.all import *
from dars.backend import createComp
# Traditional: State with component object
existing_text = Text("0", id="counter")
existing_state = State(existing_text, text=0)
# New: State with string ID (for components created later)
dynamic_state = State("dynamic-counter", text=0)
# Create the component later
create_btn.on_click = createComp(
target=Text("0", id="dynamic-counter"),
root="container-id"
)
# State works even though component was created after state!
increment_btn.on_click = dynamic_state.text.increment(by=1)
Use Cases:
- Components created with
createComp()
- Dynamically generated UIs
- Conditional component rendering
- Server-side rendered components
Access component properties through the state object to get reactive operations:
# Increment/decrement numeric properties
counter.text.increment(by=1)
counter.text.decrement(by=2)
# Set property values
counter.text.set(value=100)
# Auto operations (continuous)
counter.text.auto_increment(by=1, interval=1000) # +1 every second
counter.text.auto_decrement(by=1, interval=500) # -1 every 500ms
counter.text.stop_auto() # Stop auto operations
The
reset()
method restores all properties to their initial values:
state = State(display, text=0, style="text-blue-500")
# ... user modifies the component ...
# Reset everything back to initial state
reset_btn.on_click = state.reset()
# Increment by 1 (default)
button.on_click = counter.text.increment()
# Increment by custom amount
button.on_click = counter.text.increment(by=5)
# Decrement (negative increment)
button.on_click = counter.text.decrement(by=1)
# OR
button.on_click = counter.text.increment(by=-1)
button.on_click = counter.text.set(value=0)
State V2 supports updating all component properties , not just text:
Text Content:
state.text.set("New text")
HTML Content:
state.html.set("<strong>Bold text</strong>")
CSS Styles:
state.style.set("text-red-500 fs-[24px]")
CSS Classes:
# Set class name
state.style.set("active")
Event Handlers:
# Update event handler dynamically
state.update(on_click=alert("New handler!"))
# Or with dScript
from dars.scripts.dscript import dScript
state.update(on_click=log('clicked'))
Multiple Properties at Once:
state.update(
text="Updated!",
style="text-green-500",
on_click=alert("Done!")
)
Auto operations create continuous reactive updates:
# Auto-increment timer
timer = State(display, text=0)
start_btn.on_click = timer.text.auto_increment(by=1, interval=1000)
stop_btn.on_click = timer.text.stop_auto()
With Limits:
# Auto-increment up to 100
timer.text.auto_increment(by=1, interval=1000, max=100)
# Auto-decrement down to 0
countdown.text.auto_decrement(by=1, interval=1000, min=0)
State V2 integrates seamlessly with Dars backend HTTP utilities for reactive API-driven UIs:
from dars.all import *
from dars.backend import get, useData
# Create components
user_name = Text("", id="user-name")
user_email = Text("", id="user-email")
# Create states
name_state = State(user_name, text="")
email_state = State(user_email, text="")
# Fetch and bind API data - pure Python!
fetch_btn = Button(
"Load User",
on_click=get(
id="userData",
url="https://api.example.com/users/1",
# Access nested data with dot notation
callback=(
name_state.text.set(useData('userData').name)
.then(email_state.text.set(useData('userData').email))
)
)
)
Key Features:
-
useData('id')
- Access fetched data by operation ID
-
Dot notation
-
useData('userData').name
accesses nested properties
-
.then()
chaining
- Chain multiple state updates sequentially
-
No JavaScript
- Everything is pure Python
from dars.all import *
app = App("State V2 Demo")
# Timer display
timer_display = Text("0", id="timer", style="text-4xl")
timer = State(timer_display, text=0)
# Status display
status = Text("Paused", id="status")
status_state = State(status, text="Paused", style="paused")
# Control buttons
start_btn = Button("Start",
on_click=timer.text.auto_increment(by=1, interval=1000)
)
stop_btn = Button("Stop",
on_click=[
timer.text.stop_auto(),
status_state.update(text="Paused", style="paused")
]
)
reset_btn = Button("Reset",
on_click=[
timer.text.stop_auto(),
timer.reset(),
status_state.reset()
]
)
page = Page(Container(timer_display, status, start_btn, stop_btn, reset_btn))
app.add_page("index", page, index=True)
if __name__ == "__main__":
app.rTimeCompile()
this()
Dars introduces dynamic state updates, allowing you to modify component properties directly without pre-registering state indices.
this()
helper
The
this()
helper allows a component to refer to itself in an event handler and apply updates dynamically.
from dars.core.state import this
btn = Button("Click me", on_click=this().state(text="Clicked!", style="text-red-500"))
Supported dynamic properties:
-
text
: Update text content.
-
html
: Update inner HTML.
-
style
: Dictionary of CSS styles.
-
attrs
: Dictionary of attributes.
-
classes
: Dictionary with
add
,
remove
, or
toggle
(single string or list of strings).
this().state(
text="Updated",
style="bg-slate-100",
classes={"add": ["active"], "remove": ["inactive"]}
)
RawJS
)
You can pass raw JavaScript variables to dynamic updates using
RawJS
. This is particularly useful when:
- Chaining scripts where a previous script returns a value
- Working with async operations like file reading
- Using
dScript.ARG
to reference values from previous scripts
from dars.scripts.dscript import RawJS, Arg
from dars.desktop import read_text
# Read file -> Update component with result using the Arg helper
read_op = read_text("data.txt")
update_op = this().state(text=Arg)
chained = read_op.then(update_op)
# Using custom JavaScript expressions
this().state(text=RawJS("someVar + ' processed'"))
this()
When:
Dars Framework introduces a Hooks system inspired by React, enabling reactive and stateful behavior in both FunctionComponents and built-in components.
Hooks provide a way to add reactive capabilities to your application. They enable features like:
[!IMPORTANT] When using
Stateobjects with hooks likeuseDynamicanduseValue, the state ID should NOT match any component ID in your DOM. The state ID is a unique identifier for the state object itself, not a component.
The reactive system uses
watchers
to update components when state changes. When you create a
State
object, the ID you provide is used to register the state in the internal registry, not to identify a specific DOM element.
X Incorrect - State ID matches component ID:
# DON'T do this
state = State("my-button", count=0, disabled=False)
Button(id="my-button", text=useDynamic("my-button.count"))
In this example, both the state and the button have the ID
"my-button"
, which can cause confusion and unexpected behavior.
✓ Correct - State has unique ID:
# DO this - give state a descriptive, unique ID
counter_state = State("counter-state", count=0, disabled=False)
Button(id="my-button", text=useDynamic("counter-state.count"))
Button(id="another-button", disabled=useDynamic("counter-state.disabled"))
✓ Also Correct - Multiple components sharing same state:
# One state can control multiple components
ui_state = State("ui", count=0, is_disabled=False, message="Hello")
Container(
Text(text=useDynamic("ui.message")),
Button(id="btn-1", disabled=useDynamic("ui.is_disabled")),
Button(id="btn-2", disabled=useDynamic("ui.is_disabled")),
Text(text=useDynamic("ui.count"))
)
"user-data"
,
"cart-state"
,
"ui-controls"
)
The
useValue()
hook allows you to access the
initial value
of a state property without creating a reactive binding. This is ideal for form inputs where you want to set a default value but allow the user to edit it freely.
Pass
useValue()
to component properties to set their initial value from state:
from dars.all import *
userState = State("user", name="John Doe", email="john@example.com")
# Input with initial value from state (editable by user)
Input(value=useValue("user.name"))
# Textarea with initial value
Textarea(value=useValue("user.email"))
useValue()
supports automatic selector application in FunctionComponents! When you provide a selector (class or ID), it will be automatically applied to the element where the value is used.
from dars.all import *
app = App("Example of hooks")
userState = State("user", name="Jane Doe", email="jane@example.com", display="None")
@FunctionComponent
def UserForm(**props):
return f'''
<div {Props.id} {Props.class_name} {Props.style}>
<input value="{useValue("user.name", ".name-input")}" />
<input value="{useValue("user.email", "#email-field")}" />
<span>{useValue("user.age", ".age-display")}</span>
<span>{useDynamic("user.display")}</span>
</div>
'''
@route("/")
def index():
return Page(
UserForm(id="user-form"),
# Extract values using V() helper with the selectors
Button(
"Get Name",
on_click=userState.display.set(
"Name: " + V(".name-input") # Extract current value
)
),
Button(
"Combine Values",
on_click=userState.display.set(
V(".name-input") + " (" + V("#email-field") + ")"
)
)
)
app.add_page("index", index(), title="hooks", index=True)
if __name__ == "__main__":
app.rTimeCompile()
How it works:
1.
useValue("user.name", ".name-input")
sets initial value "Jane Doe" and applies class
name-input
to the input
2. User can edit the value freely
3.
V(".name-input")
extracts the current value (even if modified by user)
4. Perfect for forms where you need both initial values and value extraction
Supported selectors:
-
Class selectors
(
.foo
) → Added to element's
class
attribute
-
ID selectors
(
#bar
) → Set as element's
id
attribute
useDynamic("state.prop")
: Creates a
reactive binding
. If the state changes, the input value updates automatically.
useValue("state.prop")
: Sets the
initial value only
. If the state changes later, the input value does NOT update. This prevents overwriting user input while they are typing.
useValue(state_path: str, selector: str = None) -> ValueMarker
Parameters:
-
state_path
: Dot-notation path to state property (e.g.,
"user.name"
)
-
selector
: Optional CSS selector (class or ID) to apply to the element
Returns:
-
ValueMarker
object that resolves to the initial value during component rendering.
The
useDynamic()
hook creates reactive bindings between external
State
objects and component properties.
You can pass
useDynamic()
directly to properties of built-in components like
Text
,
Button
,
Input
, etc.
from dars.all import *
# Create state
userState = State("user", name="John Doe", status="Active", is_admin=False)
# Bind directly to props
card = Container(
# Bind text property
Text(text=useDynamic("user.name"), style="font-bold"),
# Bind input value
Input(value=useDynamic("user.name"), placeholder="Edit name"),
# Bind button text and disabled state
Button(
text=useDynamic("user.status"),
disabled=useDynamic("user.is_admin"),
on_click=userState.status.set("Clicked!")
)
)
useDynamic
and
useValue
supports binding to the following properties on built-in components:
| Component | Properties |
|---|---|
Text
|
text
,
innerHTML
|
Button
|
text
,
disabled
|
Input
|
value
,
placeholder
,
disabled
,
readonly
,
required
|
Textarea
|
value
,
placeholder
,
disabled
,
readonly
,
required
|
Image
|
src
,
alt
|
Link
|
href
,
text
|
Checkbox
|
checked
,
disabled
,
required
|
RadioButton
|
checked
,
disabled
,
required
|
Select
|
disabled
,
required
|
Slider
|
disabled
|
Boolean attributes like
disabled
and
checked
will be toggled based on the truthiness of the state value.
You can also use
useDynamic()
within
FunctionComponent
templates to create reactive spans.
@FunctionComponent
def UserCard(**props):
return f'''
<div {Props.id} {Props.class_name} {Props.style}>
<h3>Name: {useDynamic("user.name")}</h3>
<p>Status: {useDynamic("user.status")}</p>
</div>
'''
useDynamic(state_path: str) -> DynamicBinding
Parameters:
-
state_path
: Dot-notation path to state property (e.g.,
"user.name"
,
"cart.total"
)
Returns:
-
DynamicBinding
object that resolves to the current value during render and updates automatically when state changes.
The
useWatch()
hook allows you to monitor state changes and execute callbacks (side effects). It supports watching single or multiple state properties and executing one or more callbacks.
The recommended way to use
useWatch
is via the
app.useWatch()
or
page.useWatch()
methods:
Single State Property
from dars.all import *
cartState = State("cart", count=0, total=0.0)
# Logs to console whenever cart.count changes
app.useWatch("cart.count", log("Cart updated!"))
app.useWatch("cart.total", log("Total changed"))
Multiple State Properties (Array Syntax)
productState = State("product", name="Widget", price=19.99, info="")
# Watch multiple properties - callback executes when ANY of them change
app.useWatch(
["product.name", "product.price"],
productState.info.set("Product: " + V("product.name") + " - $" + V("product.price"))
)
Multiple Callbacks
# Execute multiple callbacks when state changes
app.useWatch(
"cart.total",
log("Total changed!"),
alert("Cart updated")
)
# Combine array syntax with multiple callbacks
app.useWatch(
["product.name", "product.price"],
productState.info.set("Product: " + V("product.name") + " - $" + V("product.price")),
log("Product info updated")
)
Page-Specific Watchers (page.useWatch)
@route("/cart")
def cart_page():
page = Page()
# This watcher only runs on the cart page
page.useWatch("cart.total", log("Total changed!"))
page.add(
Container(
Text(useDynamic("cart.total"))
)
)
return page
You can also use the classic syntax with
add_script
:
app.add_script(useWatch("state.prop", log("Changed!")))
useWatch(
state_path: Union[str, List[str]],
*callbacks: Union[dScript, str, Callable]
) -> Union[dScript, WatchMarker]
Parameters:
-
state_path
: State property path(s) to watch. Can be:
- Single path string (e.g.,
"user.name"
)
- List of paths (e.g.,
["product.name", "product.price"]
)
-
*callbacks
: One or more callbacks to execute when state changes. Each can be:
-
dScript
object (e.g.,
log("Changed")
,
alert("Update")
)
- State setter (e.g.,
productState.info.set(...)
)
- Inline JavaScript string
- Python callable returning a
dScript
Behavior:
- When using an array of state paths, the callback(s) execute when
any
of the watched properties change
- Multiple callbacks execute in the order they are provided
- Callbacks can access current state values using
V()
helper
Dars provides a set of helpers to make working with DOM values and reactive state completely Pythonic, eliminating the need for raw JavaScript.
The
V()
helper allows you to extract values from
DOM elements
(via CSS selectors) or
reactive state
(via state paths).
from dars.all import *
# Select by ID
V("#myInput")
# Select by Class
V(".myClass")
New in v1.5.8
:
V()
now supports extracting values directly from reactive state created by
useDynamic()
:
# Extract from reactive state
V("cart.total") # Gets current value of cart.total
V("user.name") # Gets current value of user.name
V("product.price") # Gets current value of product.price
How it works:
-
V("cart.total")
finds the reactive element created by
useDynamic("cart.total")
- Reads its current
textContent
value
- Perfect for combining reactive state with calculations
You can chain transformation methods to process values before using them:
# String transformations
V("#name").upper() # "JOHN"
V("#name").lower() # "john"
V("#name").trim() # Remove whitespace
# Numeric transformations (required for math operations!)
V("#age").int() # 25 (integer)
V("#price").float() # 19.99 (float)
V("cart.total").float() # Extract state value as float
V()
now supports declarative mathematical expressions with operator overloading!
# Simple arithmetic
calc.result.set(V(".a").float() + V(".b").float())
# Complex expressions with automatic precedence
calc.result.set(
(V(".a").float() + V(".b").float()) * V(".c").float()
)
# Dynamic operators from Select elements
calc.result.set(
V(".num1").float() + V(".operation").operator() + V(".num2").float()
)
Features:
- Operator overloading (
+
,
-
,
*
,
/
,
%
,
**
)
- Automatic operator precedence
- Dynamic operators from Select/Input
- NaN validation with console warnings
- Type safety (numeric ops require
.float()
or
.int()
)
[!TIP] For complete documentation on mathematical operations, operator precedence, dynamic operators, and advanced examples, see the Mathematical Operations docs.
Sometimes you want to normalize a value (literal or expression) to safely combine it within an expression with
V()
without worrying about precedence or operators:
from dars.hooks.value_helpers import V, equal
# Add 1 using V() + literal
updateVRef(".dyn_count", V(".dyn_count").int() + 1)
# Normalize a literal as a mathematical expression
updateVRef(".dyn_count", equal(0)) # forces to 0
# Combine with another expression based on V()
expr = V(".a").int() + equal(V(".b").int())
updateVRef(".result", expr)
equal(value)
wraps the value in a
MathExpression
, so it integrates into the same operation tree as
V()
and respects the async/NaN-safe semantics of the expression system.
from dars.all import *
app = App("Shopping Cart")
# Reactive state
cartState = State("cart", total=0.0)
productState = State("product", name="Widget", price=19.99, quantity=1)
@FunctionComponent
def ProductCard(**props):
return f'''
<div {Props.id} {Props.class_name} {Props.style}>
<!-- Reactive display -->
<h3>{useDynamic("product.name")}</h3>
<p>Price: ${useDynamic("product.price")}</p>
<!-- Editable quantity with selector -->
<input type="number"
value="{useValue("product.quantity", ".qty-input")}"
min="1" />
<!-- Reactive total -->
<p>Total: ${useDynamic("cart.total")}</p>
</div>
'''
@route("/")
def index():
return Page(
ProductCard(id="product-card", name="Milk", price=100, quantity=2, total=0),
# Calculate: DOM input × State value
Button("Calculate Total", on_click=cartState.total.set(
V(".qty-input").int() * V("product.price").float()
)),
# String concatenation (no transformation needed)
Button("Show Info", on_click=productState.name.set(
"Product: " + V("product.name") + " - $" + V("product.price")
)
)
)
app.add_page("index", index(), title="Product", index=True)
# Watch for changes
app.useWatch("cart.total", log("Cart total changed!"))
if __name__ == "__main__":
app.rTimeCompile()
The
url()
helper constructs dynamic URLs by interpolating
ValueRef
objects into a template string.
# Generates: https://api.example.com/users/123/profile
fetch(
url("https://api.example.com/users/{id}/profile", id=V("#userId"))
)
# With state values
fetch(
url("/api/products/{id}", id=V("product.id"))
)
# Mixed
fetch(
url("/api/{resource}/{id}",
resource="users",
id=V("#userId"))
)
Note:
Use standard Python format string syntax
{key}
for placeholders.
V()
supports boolean and comparison operations, enabling declarative validation and conditional logic without raw JavaScript!
Compare values using Python-style operators:
from dars.all import *
# Numeric comparisons (require .int() or .float())
V("#age").int() >= 18
V("#price").float() < 100.0
V("#quantity").int() == 5
# String equality
V("#password") == V("#confirm-password")
V("#email") != ""
# All operators: ==, !=, >, <, >=, <=
Check string properties with built-in methods:
# Check if string contains substring
V("#email").includes("@")
# Check string start/end
V("#filename").startswith("report_")
V("#filename").endswith(".pdf")
# Get string length (returns ValueRef with .int())
V("#password").length() >= 8
# Convert to boolean
V("#checkbox").bool()
Combine boolean expressions with
.and_()
and
.or_()
:
# AND operator
(V("#age").int() >= 18).and_(V("#age").int() <= 65)
# OR operator
(V("#email").includes("@")).or_(V("#phone").length() >= 10)
# Complex combinations
(V("#name").length() >= 3).and_(
(V("#email").includes("@")).and_(
V("#email").includes(".")
)
)
Use
.then()
for ternary operations (condition ? trueVal : falseVal):
# Simple conditional
(V("#age").int() >= 18).then("Adult", "Minor")
# With state updates
state.message.set(
(V("#score").int() >= 60).then("Pass", "Fail")
)
# Nested conditionals
(V("#premium").bool()).then("10% discount", "No discount")
# Complex validation
state.validation.set(
(V("#password").length() >= 8).and_(
V("#password") == V("#confirm")
).then("✓ Valid", "✗ Invalid")
)
from dars.all import *
app = App("Form Validation")
form = State("form",
email_valid="",
age_valid="",
password_valid=""
)
@route("/")
def index():
return Page(
Container(
# Email validation
Input(id="email", placeholder="Email"),
Button(
"Validate Email",
on_click=form.email_valid.set(
(V("#email").includes("@")).and_(
V("#email").includes(".")
).then("✓ Valid email", "✗ Invalid email")
)
),
Text(text=useDynamic("form.email_valid")),
# Age validation
Input(id="age", input_type="number", placeholder="Age"),
Button(
"Validate Age",
on_click=form.age_valid.set(
(V("#age").int() >= 18).and_(
V("#age").int() <= 120
).then("✓ Valid age", "✗ Must be 18-120")
)
),
Text(text=useDynamic("form.age_valid")),
# Password match validation
Input(id="password", input_type="password", placeholder="Password"),
Input(id="confirm", input_type="password", placeholder="Confirm"),
Button(
"Check Match",
on_click=form.password_valid.set(
(V("#password") == V("#confirm")).and_(
V("#password").length() >= 8
).then("✓ Passwords match", "✗ Passwords don't match")
)
),
Text(text=useDynamic("form.password_valid"))
)
)
app.add_page("index", index())
Pythonic form data collection and submission without raw JavaScript!
The
FormData
class and
collect_form()
helper provide a declarative way to collect form data using
V()
expressions.
from dars.all import *
# Collect form data with kwargs syntax
form_data = collect_form(
name=V("#name-input"),
email=V("#email-input"),
age=V("#age-input").int(),
is_premium=V("#premium-checkbox")
)
# Show in alert
Button("Submit", on_click=form_data.alert())
# Log to console
Button("Log", on_click=form_data.log())
# Save to state
Button("Save", on_click=form_data.to_state(state.data))
Nested Dictionaries & Lists:
form_data = collect_form(
name=V("#name"),
email=V("#email"),
# Nested validation results
validation={
"email_valid": V("#email").includes("@"),
"age_ok": (V("#age").int() >= 18).and_(
V("#age").int() <= 120)
},
# Conditional values
discount=(V("#premium").bool()).then("10%", "0%"),
# Timestamp
submitted_at=getDateTime()
)
Alternative Syntaxes:
# Using tuples
form_data = collect_form(
("name", V("#name")),
("email", V("#email"))
)
# Using dict
form_data = collect_form({
"name": V("#name"),
"email": V("#email")
})
.alert(title)
- Show form data in alert dialog:
Button("Show Data", on_click=form_data.alert("Form Data"))
.log(message)
- Log form data to console:
Button("Log Data", on_click=form_data.log("Form submitted"))
.to_state(property)
- Save form data to state:
Button("Save", on_click=form_data.to_state(state.form_data))
.submit(url, state_property, on_success, on_error)
- Submit to backend:
# Simple submit
Button("Submit", on_click=form_data.submit("http://localhost:3000/submit"))
# With state and callbacks
Button("Submit", on_click=form_data.submit(
url="http://localhost:3000/submit",
state_property=state.response,
on_success=alert("Success!"),
on_error=alert("Error!")
))
.submit_and_alert(state_property, title)
- Submit with alert:
Button("Submit", on_click=form_data.submit_and_alert(
state.data,
"Form Submitted!"
))
from dars.all import *
app = App("Form with Backend")
form = State("form", response="")
# Collect form data
form_data = collect_form(
name=V("#name"),
email=V("#email"),
age=V("#age").int(),
submitted_at=getDateTime()
)
@route("/")
def index():
return Page(
Container(
Input(id="name", placeholder="Name"),
Input(id="email", placeholder="Email"),
Input(id="age", input_type="number", placeholder="Age"),
# Submit to backend
Button(
"Submit to Backend",
on_click=form_data.submit(
url="http://localhost:3000/submit",
state_property=form.response,
on_success=alert("Form submitted successfully!")
)
),
# Display backend response
Container(
Text("Backend Response:", style="font-bold"),
Text(text=useDynamic("form.response"))
)
)
)
app.add_page("index", index())
The
setVRef()
hook allows you to define initial values that are tied to a specific CSS selector. This is the foundation for creating component-level state that can be shared across multiple components without using global
State
objects.
Create a reference with an initial value and a selector, then pass it to components.
from dars.all import *
# Create a reference tied to the "#count" ID
count_ref = setVRef(0, "#count")
# Use it in a component
Text(count_ref, id="count")
By using a class selector , you can share the same value across multiple components and update them all simultaneously!
# Create a reference tied to a CLASS selector
price_ref = setVRef(99.99, ".product-price")
# Use in multiple places
Container(
Text("Price: $", style="font-bold"),
Text(price_ref, class_name="product-price"), # Main display
Container(
Text("Also shown here: $"),
Text(price_ref, class_name="product-price") # Secondary display
)
)
# Update ALL elements matching ".product-price" at once
Button("Discount", on_click=updateVRef(".product-price", 49.99))
setVRef
works seamlessly with
@FunctionComponent
. The value is resolved internally, so your templates remain clean.
@FunctionComponent
def UserBadge(name_ref, **props):
return f'''
<div {Props.class_name} {Props.style}>
User: <span class="user-name">{name_ref}</span>
</div>
'''
# Define ref
user_ref = setVRef("Guest", ".user-name")
# Render
UserBadge(user_ref, class_name="badge")
# Update
Button("Login", on_click=updateVRef(".user-name", "John Doe"))
setVRef(initial_value: Any, selector: str) -> VRefValue
Parameters:
-
initial_value
: The initial value to display (string, number, boolean).
-
selector
: The CSS selector (ID or Class) that identifies the element(s).
- Use
#id
for single elements.
- Use
.class
for multiple elements sharing the value.
Returns:
-
VRefValue
: An object representing the value, ready to be passed to components.
Update DOM element values declaratively without State objects!
The
updateVRef()
function completes the component-level state management cycle, providing a Pythonic way to update values alongside
V()
for reading and boolean operators for validation.
The Complete Cycle:
1.
Read
:
V("#input")
- Extract values
2.
Validate
:
V("#input").length() >= 3
- Boolean validation
3.
Update
:
updateVRef("#input", "new value")
- Update values ✨
NEW!
from dars.all import *
# Update text content
Button("Set Name", on_click=updateVRef("#name", "John Doe"))
# Update input value
Button("Clear Email", on_click=updateVRef("#email", ""))
# Update checkbox
Button("Check Box", on_click=updateVRef("#agree", True))
# Update number
Button("Set Price", on_click=updateVRef("#price", 99.99))
Combine
updateVRef()
with
V()
expressions for dynamic updates:
# Copy values between elements
Button("Copy", on_click=updateVRef("#target", V("#source")))
# With transformations
Button("Uppercase", on_click=updateVRef("#output", V("#input").upper()))
# With calculations
Button("Calculate Total", on_click=updateVRef("#total",
V("#price").float() * V("#qty").int()
))
# With string concatenation
Button("Generate Full Name", on_click=updateVRef("#full-name",
V("#first-name") + " " + V("#last-name")
))
Use boolean operators for conditional updates:
# Conditional text based on age
Button("Check Age", on_click=updateVRef("#status",
(V("#age").int() >= 18).then("Adult", "Minor")
))
# Validation messages
Button("Validate Email", on_click=updateVRef("#message",
(V("#email").includes("@")).and_(
V("#email").includes(".")
).then("✓ Valid email", "✗ Invalid email")
))
# Complex validation
Button("Check Password", on_click=updateVRef("#pwd-status",
(V("#password").length() >= 8).and_(
V("#password") == V("#confirm")
).then("✓ Passwords match", "✗ Passwords don't match")
))
Update multiple elements with a single call:
# Clear entire form
Button("Clear All", on_click=updateVRef({
"#name": "",
"#email": "",
"#age": "",
"#phone": ""
}))
# Fill sample data
Button("Fill Sample Data", on_click=updateVRef({
"#name": "John Doe",
"#email": "john@example.com",
"#age": 25,
"#phone": "555-0123"
}))
# Mix literals and expressions
Button("Update All", on_click=updateVRef({
"#full-name": V("#first") + " " + V("#last"),
"#email-lower": V("#email").lower(),
"#age-status": (V("#age").int() >= 18).then("Adult", "Minor")
}))
from dars.all import *
app = App("Counter Demo")
@route("/")
def index():
return Page(
Container(
# Display count
Text("Count: ", style="font-bold"),
Text("0", id="count", style="text-[48px] font-bold text-blue-600"),
# Update buttons
Container(
Button(
"+",
on_click=updateVRef("#count", V("#count").int() + 1),
style="bg-green-500 text-white px-6 py-3 rounded"
),
Button(
"-",
on_click=updateVRef("#count", V("#count").int() - 1),
style="bg-red-500 text-white px-6 py-3 rounded"
),
Button(
"Reset",
on_click=updateVRef("#count", 0),
style="bg-gray-500 text-white px-6 py-3 rounded"
),
style="flex gap-2"
)
)
)
app.add_page("index", index())
@route("/form")
def form():
return Page(
Container(
Input(id="first-name", placeholder="First Name"),
Input(id="last-name", placeholder="Last Name"),
Input(id="full-name", placeholder="Full Name", readonly=True),
# Auto-generate full name
Button(
"Generate Full Name",
on_click=updateVRef("#full-name",
V("#first-name") + " " + V("#last-name")
)
),
# Normalize inputs
Button(
"Normalize All",
on_click=updateVRef({
"#first-name": V("#first-name").trim(),
"#last-name": V("#last-name").trim()
})
),
# Clear all
Button(
"Clear All",
on_click=updateVRef({
"#first-name": "",
"#last-name": "",
"#full-name": ""
})
)
)
)
@route("/cart")
def cart():
return Page(
Container(
Input(id="price", input_type="number", value="19.99", placeholder="Price"),
Input(id="quantity", input_type="number", value="1", placeholder="Quantity"),
Text("Total: $", style="font-bold"),
Text("0", id="total", style="text-[24px] text-green-600"),
# Calculate total
Button(
"Calculate Total",
on_click=updateVRef("#total",
V("#price").float() * V("#quantity").int()
)
),
# Apply discount
Button(
"Apply 10% Discount",
on_click=updateVRef("#total",
V("#total").float() * 0.9
)
),
# Reset
Button(
"Reset",
on_click=updateVRef({
"#price": "19.99",
"#quantity": "1",
"#total": "0"
})
)
)
)
updateVRef(selector, value) -> dScript
updateVRef(dict) -> dScript
Parameters:
-
selector
: CSS selector string (e.g.,
"#id"
,
".class"
)
-
value
: Value to set - can be:
- Literal:
"text"
,
42
,
True
- V() expression:
V("#source")
- Transformation:
V("#input").upper()
- Math expression:
V("#a").int() + V("#b").int()
- Boolean expression:
(V("#age").int() >= 18).then("Adult", "Minor")
-
dict
: Dictionary of
{selector: value}
pairs for batch updates
Returns:
-
dScript
object for use in event handlers
Supported Elements:
-
Input
/
Textarea
: Updates
.value
property
-
Checkbox
/
Radio
: Updates
.checked
property
-
Select
: Updates
.value
property
- Other elements: Updates
.textContent
# Normalize before collecting
form_data = collect_form(
name=V("#name"),
email=V("#email")
)
Button(
"Normalize & Submit",
on_click=sequence(
updateVRef({
"#name": V("#name").trim(),
"#email": V("#email").lower().trim()
}),
form_data.submit("http://localhost:3000/submit")
)
)
# Local updates for preview
Button("Preview", on_click=updateVRef("#preview",
"Name: " + V("#name") + ", Email: " + V("#email")
))
# Save to global state
user = State("user", name="", email="")
Button("Save to State", on_click=sequence(
user.name.set(V("#name")),
user.email.set(V("#email"))
))
Use
updateVRef()
when:
- Updating UI elements temporarily
- Form auto-fill and normalization
- Local calculations and previews
- Component-level state
- You don't need reactivity across components
Use
State.set()
when:
- Data needs to persist
- Multiple components need the value
- You need automatic reactivity
- Application-level state
Use both (Hybrid): - Local updates for immediate feedback - State updates for persistence - Best of both worlds!
**Generate client-side timestamps for forms and state updates.
from dars.all import *
# Default ISO format
getDateTime() # "2025-12-04T22:04:09.123Z"
# Different formats
getDateTime("iso") # "2025-12-04T22:04:09.123Z"
getDateTime("locale") # "12/4/2025, 10:04:09 PM"
getDateTime("date") # "12/4/2025"
getDateTime("time") # "10:04:09 PM"
getDateTime("timestamp") # 1733362449123
# Add timestamp to form submission
form_data = collect_form(
name=V("#name"),
email=V("#email"),
submitted_at=getDateTime() # ISO format
)
# Different timestamp formats
form_data = collect_form(
name=V("#name"),
created_at=getDateTime("iso"),
display_date=getDateTime("locale"),
date_only=getDateTime("date"),
time_only=getDateTime("time"),
unix_timestamp=getDateTime("timestamp")
)
# Update state with current timestamp
Button("Save", on_click=state.last_updated.set(getDateTime()))
# Different formats
Button("Save Date", on_click=state.date.set(getDateTime("date")))
Button("Save Time", on_click=state.time.set(getDateTime("time")))
from dars.all import *
app = App("Timestamp Demo")
state = State("state", last_action="", timestamp="")
form_data = collect_form(
action=V("#action"),
timestamp=getDateTime("locale")
)
@route("/")
def index():
return Page(
Container(
Input(id="action", placeholder="What did you do?"),
Button(
"Record Action",
on_click=form_data.to_state(state.last_action)
),
Text("Last Action:", style="font-bold"),
Text(text=useDynamic("state.last_action")),
Button(
"Update Timestamp",
on_click=state.timestamp.set(getDateTime("locale"))
),
Text("Current Time:", style="font-bold"),
Text(text=useDynamic("state.timestamp"))
)
)
app.add_page("index", index())
if __name__ == "__main__":
app.rTimeCompile()
Do:
- Use
useDynamic
for simple text/value updates.
- Use
useWatch
for side effects like logging, analytics, or complex logic.
- Use
useValue
with selectors for form inputs that need value extraction.
- Use consistent state naming (e.g.,
"user"
,
"cart"
).
- Always use
.int()
or
.float()
before arithmetic operations with
V()
.
Don't:
- Use with non-existent state paths.
- Nest state paths more than 2 levels deep (currently supports
stateName.property
).
- Use arithmetic operators without numeric transformations.
Dars introduces a powerful, declarative system for mathematical expressions using operator overloading. Write complex calculations in pure Python without any inline JavaScript!
The
V()
helper supports:
-
Operator overloading
- Use Python operators (
+
,
-
,
*
,
/
,
%
,
**
)
-
Dynamic operators
- Operators from Select/Input elements
-
Automatic precedence
- Parentheses handled automatically
-
NaN validation
- Safe handling of empty/invalid inputs
-
Type safety
- Numeric operations require
.float()
or
.int()
String concatenation works without any transformations:
from dars.all import *
# Simple concatenation
state.fullname.set(V(".first") + " " + V(".last"))
# With literals
state.message.set("Hello, " + V(".username") + "!")
# Multiple values
state.info.set(V(".name") + " - " + V(".email") + " - " + V(".phone"))
IMPORTANT
: Arithmetic operations
require
.float()
or
.int()
transformations:
# Addition
state.total.set(V(".price").float() + V(".tax").float())
# Subtraction
state.change.set(V(".paid").float() - V(".total").float())
# Multiplication
state.total.set(V(".price").float() * V(".quantity").int())
# Division
state.average.set(V(".sum").float() / V(".count").int())
# Module
state.remainder.set(V(".number").int() % V(".divisor").int())
# Power
state.result.set(V(".base").float() ** V(".exponent").int())
Why transformations are required:
- Prevents accidental string concatenation (e.g.,
"5" * "3"
=
"555"
in JavaScript)
- Ensures type safety
- Makes intent explicit
Dars automatically handles operator precedence following standard mathematical rules:
# a + b * c → a + (b * c) ✅ Automatic
state.result.set(
V(".a").float() + V(".b").float() * V(".c").float()
)
# (a + b) * c → (a + b) * c ✅ Preserved
state.result.set(
(V(".a").float() + V(".b").float()) * V(".c").float()
)
# a ** b ** c → a ** (b ** c) ✅ Right-associative
state.result.set(
V(".a").float() ** V(".b").float() ** V(".c").float()
)
Precedence Table:
| Operator | Precedence | Associativity |
|---|---|---|
**
|
3 (highest) | Right |
*
,
/
,
%
|
2 | Left |
+
,
-
|
1 (lowest) | Left |
You can nest expressions infinitely:
# Complex calculation
state.result.set(
((V(".a").float() + V(".b").float()) * V(".c").float()) /
(V(".d").float() - V(".e").float())
)
# With mixed literals
state.total.set(
(V(".price").float() * V(".qty").int()) * 1.15 # Add 15% tax
)
# Combining multiple operations
state.score.set(
(V(".math").int() + V(".science").int() + V(".english").int()) / 3
)
Use operators from Select or Input elements!
.operator()
Method
Mark a
ValueRef
as a dynamic operator using
.operator()
:
# Select with operator options
Select(
class_name="operation",
options=[
SelectOption("+", "Add"),
SelectOption("-", "Subtract"),
SelectOption("*", "Multiply"),
SelectOption("/", "Divide")
]
)
# Use in calculation
Button(
"Calculate",
on_click=calc.result.set(
V(".num1").float() + V(".operation").operator() + V(".num2").float()
)
)
V(".operation").operator()
extracts the operator value
+
,
-
,
*
,
/
,
%
,
**
)
+
if operator is invalid
from dars.all import *
app = App("Calculator")
calc = State("calc", operation="+", result=0)
@route("/")
def index():
return Page(
# Operation selector
Select(
value=useValue("calc.operation", selector=".operation"),
class_name="operation",
options=[
SelectOption("+", "➕ Add"),
SelectOption("-", "➖ Subtract"),
SelectOption("*", "✖️ Multiply"),
SelectOption("/", "➗ Divide")
]
),
# Number inputs
Input(class_name="num1", input_type="number", placeholder="First number"),
Input(class_name="num2", input_type="number", placeholder="Second number"),
# Calculate button - DECLARATIVE!
Button(
"Calculate",
on_click=calc.result.set(
V(".num1").float() + V(".operation").operator() + V(".num2").float()
)
),
# Result display
Text(text=useDynamic("calc.result"))
)
app.add_page("index", index())
if __name__ == "__main__":
app.rTimeCompile()
The
.operator()
method validates against this whitelist:
+
- Addition
-
- Subtraction
*
- Multiplication
/
- Division
%
- Modulo
**
- Power
Invalid operators
automatically fall back to
+
with a console warning.
New in v1.6.4 : Automatic NaN handling prevents errors from empty or invalid inputs.
All mathematical expressions include built-in NaN validation:
# If inputs are empty or invalid
Button(
"Calculate",
on_click=calc.result.set(
V(".num1").float() + V(".num2").float()
)
)
# Empty inputs → Returns 0
# Console: "[Dars] Invalid input: one or more values are NaN. Returning 0."
# Empty inputs
V(".empty-input").float() # → 0 (with warning)
# Invalid division
V(".num").float() / 0 # → 0 (with warning)
# Invalid operation
V(".text").float() # → 0 (with warning)
MathExpression
objects work seamlessly with
State.set()
:
# Simple expression
calc.result.set(V(".a").float() + V(".b").float())
# Complex expression
calc.total.set(
(V(".price").float() * V(".qty").int()) * 1.15
)
# With dynamic operator
calc.result.set(
V(".num1").float() + V(".op").operator() + V(".num2").float()
)
All expressions are automatically wrapped in async IIFEs:
# Your code
calc.result.set(V(".a").float() + V(".b").float())
# Generated JavaScript
(async () => {
const left = await (/* V(".a").float() */);
const right = await (/* V(".b").float() */);
if (isNaN(left) || isNaN(right)) {
console.warn('[Dars] Invalid input: one or more values are NaN. Returning 0.');
return 0;
}
const result = left + right;
if (isNaN(result)) {
console.warn('[Dars] Operation resulted in NaN. Returning 0.');
return 0;
}
return result;
})()
# Calculate total with tax and discount
Button(
"Calculate Total",
on_click=cart.total.set(
((V(".price").float() * V(".qty").int()) * 1.15) - V(".discount").float()
)
)
# Different calculations based on operator
Select(class_name="calc-type", options=[
SelectOption("area", "Area"),
SelectOption("perimeter", "Perimeter")
])
# Area: length * width
# Perimeter: 2 * (length + width)
Button(
"Calculate",
on_click=dScript(f"""
const type = {V(".calc-type").get_code()};
const length = {V(".length").float().get_code()};
const width = {V(".width").float().get_code()};
let result;
if (type === 'area') {{
result = length * width;
}} else {{
result = 2 * (length + width);
}}
window.Dars.change({{
id: 'calc',
dynamic: true,
result: result
}});
""")
)
# Power calculation
calc.result.set(V(".base").float() ** V(".exponent").float())
# Modulo for remainders
calc.remainder.set(V(".dividend").int() % V(".divisor").int())
# Complex formula: (a² + b²)^0.5 (Pythagorean theorem)
calc.hypotenuse.set(
(V(".a").float() ** 2 + V(".b").float() ** 2) ** 0.5
)
Always use transformations for math
V(".price").float() * V(".qty").int() # ✅ Correct
Use descriptive class names
Input(class_name="price-input") # ✅ Clear
V(".price-input").float()
Leverage automatic precedence
V(".a").float() + V(".b").float() * V(".c").float() # ✅ Auto-parens
Use dynamic operators for flexibility
V(".num1").float() + V(".op").operator() + V(".num2").float() # ✅ Dynamic
Don't use arithmetic without transformations
V(".price") * V(".qty") # ❌ TypeError
Don't rely on implicit type coercion
V(".number") + 5 # ❌ String concatenation, not addition
Don't forget NaN is handled automatically
# No need for manual checks
if (isNaN(V(".num").float())) { ... }
# Automatic validation
calc.result.set(V(".num").float() + 10)
Created by
V(selector)
:
class ValueRef:
def int() -> ValueRef # Convert to integer
def float() -> ValueRef # Convert to float
def upper() -> ValueRef # Convert to uppercase
def lower() -> ValueRef # Convert to lowercase
def trim() -> ValueRef # Remove whitespace
def operator() -> DynamicOperator # Mark as dynamic operator
# Operators
def __add__(other) -> MathExpression
def __sub__(other) -> MathExpression
def __mul__(other) -> MathExpression
def __truediv__(other) -> MathExpression
def __mod__(other) -> MathExpression
def __pow__(other) -> MathExpression
Created by operator overloading:
class MathExpression:
# Supports all arithmetic operators
# Automatically handles precedence
# Validates NaN
# Generates async JavaScript
Created by
.operator()
:
class DynamicOperator:
VALID_OPERATORS = ['+', '-', '*', '/', '%', '**']
# Validates operator at runtime
# Falls back to '+' if invalid
Dars Framework provides a powerful,
Pythonic system
for handling HTTP requests and API communication without writing any JavaScript. The
dars.backend
module enables you to fetch data, bind it to components, and create reactive UIs entirely in Python.
from dars.all import *
from dars.backend import get, useData
app = App(title="API Demo")
# Create display component
user_display = Text("No data", id="user-name")
user_state = State(user_display, text="No data")
# Fetch and bind data - pure Python!
fetch_btn = Button(
"Fetch User",
on_click=get(
id="userData",
url="https://api.example.com/user/1",
callback=user_state.text.set(useData('userData').name)
)
)
app.set_root(Container(user_display, fetch_btn))
if __name__ == "__main__":
app.rTimeCompile()
The
dars.backend
module provides standard HTTP methods that return
dScript
objects:
get(id, url, **options)
Performs a GET request.
from dars.backend import get
# Basic GET
get_user = get(
id="userData",
url="https://jsonplaceholder.typicode.com/users/1"
)
# With callback
get_user = get(
id="userData",
url="https://api.example.com/user/1",
callback=status_state.text.set("✅ Loaded!"),
on_error=status_state.text.set("❌ Error!")
)
post(id, url, body, **options)
Performs a POST request.
from dars.backend import post
# POST with JSON body
create_user = post(
id="createResult",
url="https://api.example.com/users",
body={"name": "John", "email": "john@example.com"},
callback=status_state.text.set("User created!")
)
put(id, url, body, **options)
- Update resource
delete(id, url, **options)
- Delete resource
patch(id, url, body, **options)
- Partial update
fetch(id, url, method, **options)
- Generic fetch
All HTTP functions accept these options:
| Option | Type | Description |
|---|---|---|
id
|
str
|
Required . Operation ID (NOT HTML ID) for accessing data |
url
|
str
|
Required . API endpoint URL |
headers
|
dict
|
Custom HTTP headers |
callback
|
dScript
|
Executed on success |
on_error
|
dScript
|
Executed on error |
parse_json
|
bool
|
Auto-parse JSON response (default:
True
)
|
timeout
|
int
|
Request timeout in milliseconds |
The
useData()
function provides
Pythonic access
to fetched data using dot notation:
from dars.backend import useData
# Access fetched data by operation ID
user_data = useData('userData')
# Access nested properties with dot notation
user_name = useData('userData').name
user_email = useData('userData').email
user_address_city = useData('userData').address.city
get(id="userData", ...)
, the response is stored in
window.userData
useData('userData')
creates a
DataAccessor
object
.name
uses
__getattr__
to create
window.userData?.name
.code
property generates the JavaScript expression
The most powerful feature is binding API data directly to component states:
from dars.all import *
from dars.backend import get, useData
# Create components and states
name_display = Text("", id="user-name")
email_display = Text("", id="user-email")
name_state = State(name_display, text="")
email_state = State(email_display, text="")
# Fetch and bind - pure Python!
fetch_button = Button(
"Fetch User",
on_click=get(
id="userData",
url="https://jsonplaceholder.typicode.com/users/1",
# Chain multiple state updates with .then()
callback=(
name_state.text.set(useData('userData').name)
.then(email_state.text.set(useData('userData').email))
)
)
)
.then()
Chain multiple operations sequentially:
# Update multiple components
callback=(
status_state.text.set("Loading...")
.then(name_state.text.set(useData('userData').name))
.then(email_state.text.set(useData('userData').email))
.then(status_state.text.set("✅ Loaded!"))
)
Helper functions for working with JSON data:
stringify(data, pretty=False)
Convert data to JSON string:
from dars.backend import stringify, useData
# Stringify fetched data
display_state.text.set(stringify(useData('userData'), pretty=True))
# Stringify Python objects
json_str = stringify({"name": "John", "age": 30})
parse(json_string)
Parse JSON string:
from dars.backend import parse
# Parse JSON string
data = parse('{"name": "John"}')
get_value(obj, path, default=None)
Safely access nested values:
from dars.backend import get_value, useData
# Safe nested access with default
city = get_value(useData('userData'), 'address.city', default='Unknown')
Create, update, and delete components dynamically at runtime:
createComp(target, root, position="append")
Create a new component in the DOM:
from dars.backend import createComp
# Create new component
new_text = Text("Hello!", id="new-item")
create_btn.on_click = createComp(
target=new_text,
root="container-id",
position="append" # or "prepend", "before:id", "after:id"
)
Tip:
You can create a
State()
for a component before it exists using a string ID:
# Create state with string ID
item_state = State("new-item", text="Hello!")
# Create component later
create_btn.on_click = createComp(
target=Text("Hello!", id="new-item"),
root="container-id"
)
# State works immediately!
update_btn.on_click = item_state.text.set("Updated!")
updateComp(target, **props)
Update component properties:
from dars.backend import updateComp
# Update component
update_btn.on_click = updateComp(
"my-component-id",
text="Updated!",
style="text-red-500"
)
deleteComp(id)
Remove a component from the DOM:
from dars.backend import deleteComp
# Delete component
delete_btn.on_click = deleteComp("component-id")
id
to avoid conflicts
.then()
to chain multiple state updates sequentially
on_error
callbacks for better UX
get(id, url, **options)
- GET request
post(id, url, body, **options)
- POST request
put(id, url, body, **options)
- PUT request
delete(id, url, **options)
- DELETE request
patch(id, url, body, **options)
- PATCH request
fetch(id, url, method, **options)
- Generic fetch
useData(operation_id)
- Access fetched data with dot notation
stringify(data, pretty=False)
- Convert to JSON string
parse(json_string)
- Parse JSON
get_value(obj, path, default=None)
- Safe nested access
createComp(target, root, position)
- Create component
updateComp(target, **props)
- Update component
deleteComp(id)
- Delete component
For more examples, see the test files in
tst/proj/test_http_demo.py
and
tst/proj/test_http_utils.py
.
Dars provides a built-in
create_ssr_app
helper to easily serve your Dars application with Server-Side Rendering (SSR) using FastAPI.
When you run
dars init --type ssr
, Dars creates a backend structure for you:
*
backend/api.py
: Entry point for the FastAPI server (Default Port: 8000).
*
backend/apiConfig.py
: Configuration helper for environment management.
apiConfig.py
)
To switch between Development and Production modes, simply edit the
MODE
variable in
backend/apiConfig.py
:
class DarsEnv:
# Set this to "production" when deploying
MODE = "development"
DEV = "development"
BUILD = "production"
# ...
localhost:8000
, Frontend on
localhost:3000
.
dars dev
proxies requests.
# Start the SSR Backend (Port 3000)
python backend/api.py
In a separate terminal, run the frontend dev server:
# Start Frontend Dev Server (Port 8000)
dars dev
This is the documentation for the events in Dars.
Custom components in Dars can have events associated with them. You can set an event on a custom component using the
set_event
method.
self.set_event(EventTypes.CLICK, log('click'))
To use the event types, you need to import them from
dars.core.events
:
from dars.core.events import EventTypes
Here are the different event types available:
Mouse Events:
CLICK = "click"
DOUBLE_CLICK = "dblclick"
MOUSE_DOWN = "mousedown"
MOUSE_UP = "mouseup"
MOUSE_ENTER = "mouseenter"
MOUSE_LEAVE = "mouseleave"
MOUSE_MOVE = "mousemove"
Keyboard Events:
KEY_DOWN = "keydown"
KEY_UP = "keyup"
KEY_PRESS = "keypress"
Form Events:
CHANGE = "change"
INPUT = "input"
SUBMIT = "submit"
FOCUS = "focus"
BLUR = "blur"
Load Events:
LOAD = "load"
ERROR = "error"
RESIZE = "resize"
on_*
attribute can now accept:
Example using
Mod.set
:
Mod.set("btn1", on_click=[st1.state(0), log('clicked')])
Runtime behavior:
Mod.set
replaces the previous one.
Dars provides HTTP utilities that can be used directly in event handlers:
from dars.all import *
from dars.backend import get, post, useData
# GET request on button click
fetch_btn = Button(
"Fetch Data",
on_click=get(
id="apiData",
url="https://api.example.com/data",
callback=status_state.text.set("✅ Loaded!")
)
)
# POST request with data binding
submit_btn = Button(
"Submit",
on_click=post(
id="submitResult",
url="https://api.example.com/submit",
body={"name": "John", "email": "john@example.com"},
callback=result_state.text.set(useData('submitResult').message)
)
)
# Chain HTTP request with state updates
button.on_click = [
status_state.text.set("Loading..."),
get(
id="userData",
url="https://api.example.com/user/1",
callback=(
name_state.text.set(useData('userData').name)
.then(status_state.text.set("Done!"))
)
)
]
Dars provides a powerful and intuitive system for handling keyboard events in your applications. This guide covers everything from basic key detection to advanced global shortcuts.
All Dars components support the
on_key_press
event handler for keyboard interactions.
from dars.all import *
Input(
id="search",
on_key_press=log("Key pressed!")
)
Note: Use
on_key_pressas the universal keyboard event. The olderon_key_downandon_key_upevents have been deprecated in favor of this simpler approach.
The
KeyCode
class provides constants for all keyboard keys, making your code more readable and maintainable.
from dars.all import *
# Navigation keys
KeyCode.ENTER
KeyCode.TAB
KeyCode.ESCAPE # or KeyCode.ESC
KeyCode.BACKSPACE
KeyCode.DELETE
# Arrow keys
KeyCode.UP # or KeyCode.ARROWUP
KeyCode.DOWN # or KeyCode.ARROWDOWN
KeyCode.LEFT # or KeyCode.ARROWLEFT
KeyCode.RIGHT # or KeyCode.ARROWRIGHT
# Letters (a-z)
KeyCode.A
KeyCode.B
# ... through ...
KeyCode.Z
# Numbers
KeyCode.ZERO # or KeyCode.0
KeyCode.ONE # or KeyCode.1
# ... through ...
KeyCode.NINE # or KeyCode.9
# Function keys
KeyCode.F1
KeyCode.F2
# ... through ...
KeyCode.F12
# Special characters
KeyCode.SPACE
KeyCode.PLUS
KeyCode.MINUS
KeyCode.SLASH
KeyCode.COMMA
KeyCode.PERIOD
# Get key code by name
key = KeyCode.key('enter') # Returns 'Enter'
key = KeyCode.key('A') # Returns 'a'
The
onKey()
function is the
recommended way
to handle specific keyboard keys with optional modifier keys.
from dars.all import *
# Simple key detection
Input(
on_key_press=onKey(KeyCode.ENTER, log("Enter pressed!"))
)
# Ctrl modifier
Container(
on_key_press=onKey(KeyCode.S, alert("Saving..."), ctrl=True)
)
# Multiple modifiers
Container(
on_key_press=onKey(KeyCode.Z, log("Redo"), ctrl=True, shift=True)
)
ctrl
- Ctrl key (Command on Mac)
shift
- Shift key
alt
- Alt key
meta
- Meta/Command key
from dars.all import *
app = App("onKey Example")
formState = State("form", message="")
@route("/")
def index():
return Page(
Input(
id="input",
placeholder="Press Enter to submit",
on_key_press=onKey(
KeyCode.ENTER,
formState.message.set("Submitted!"),
ctrl=False # Just Enter, no modifier needed
)
),
Text(useDynamic("form.message"))
)
app.add_page("index", index(), index=True)
Use
switch()
to handle multiple different keys in a single event handler.
from dars.all import *
Input(
on_key_press=switch({
KeyCode.ENTER: log("Enter pressed"),
KeyCode.ESCAPE: alert("Escape pressed"),
})
)
formState = State("form", username="", password="")
Input(
on_key_press=switch({
KeyCode.ENTER: [
formState.message.set("Submitted!"),
alert("Form submitted!")
],
KeyCode.ESCAPE: [
clearInput("username"),
clearInput("password"),
formState.message.set("Cleared!")
]
})
)
from dars.all import *
app = App("KeyCode Clean Example")
# State
formState = State("form",
username="",
password="",
message="Use keyboard shortcuts!"
)
@FunctionComponent
def LoginForm(**props):
return f'''
<div {Props.id} {Props.class_name} {Props.style}>
<h2>Login Form with Keyboard Shortcuts</h2>
<p style="color: #666;">{useDynamic("form.message")}</p>
<input
type="text"
id="username-input"
placeholder="Username"
style="display: block; margin: 10px 0; padding: 8px; width: 300px;"
/>
<input
type="password"
id="password-input"
placeholder="Password"
style="display: block; margin: 10px 0; padding: 8px; width: 300px;"
/>
<div style="margin-top: 30px; padding: 15px; background: #f5f5f5; border-radius: 4px;">
<h3 style="margin-top: 0;">Global Keyboard Shortcuts:</h3>
<ul style="margin: 0; padding-left: 20px;">
<li><strong>Ctrl+Enter</strong> - Submit form (shows alert)</li>
<li><strong>Ctrl+F</strong> - Clear form</li>
<li><strong>Ctrl+S</strong> - Save document</li>
</ul>
<p style="margin-top: 10px; font-size: 0.9em; color: #666;">
Note: These are GLOBAL shortcuts that work anywhere on the page without blocking normal typing.
</p>
</div>
</div>
'''
@route("/")
def index():
return Page(
LoginForm(id="login-form"),
Input(value="", on_key_up=onKey("R", action=log("Logged"))),
# Buttons using State.set() and utils_ds functions
Button(
"Submit",
on_click=[
formState.message.set("Form submitted via button!"),
alert("Form submitted!")
]
),
Button(
"Clear",
on_click=[
formState.username.set(""),
formState.password.set(""),
formState.message.set("Form cleared via button!"),
clearInput("username-input"),
clearInput("password-input")
]
),
Button(
"Show Username",
on_click=alert(V("#username-input"))
),
Button(
"Log Message",
on_click=log(V("form.message"))
),
)
addGlobalKeys(app, {
(KeyCode.ENTER, 'ctrl'): [
formState.message.set("Form submitted with Ctrl+Enter!"),
alert("Form submitted!")
],
(KeyCode.F, 'ctrl'): [
formState.username.set(""),
formState.password.set(""),
formState.message.set("Form cleared with Ctrl+F!"),
clearInput("username-input"),
clearInput("password-input")
],
(KeyCode.S, 'ctrl'): alert("Document saved! (Ctrl+S)"),
})
app.add_page("index", index(), title="KeyCode Example", index=True)
if __name__ == "__main__":
app.rTimeCompile()
Use
addGlobalKeys()
to create app-wide keyboard shortcuts that work anywhere on the page.
Global shortcuts are perfect for: - App-level commands (Save, Undo, Redo) - Navigation shortcuts - Quick actions that should work anywhere
Important: Always use modifier keys (Ctrl, Alt, etc.) with global shortcuts to avoid blocking normal typing in input fields.
from dars.all import *
app = App("Global Shortcuts")
# Define your actions
def save_document():
return alert("Document saved!")
def undo():
return log("Undo action")
# Add global shortcuts
addGlobalKeys(app, {
(KeyCode.S, 'ctrl'): save_document(),
(KeyCode.Z, 'ctrl'): undo()
})
formState = State("form", data="")
addGlobalKeys(app, {
(KeyCode.ENTER, 'ctrl'): [
formState.data.set("Submitted!"),
alert("Form submitted with Ctrl+Enter")
],
(KeyCode.ESCAPE, 'ctrl'): [
formState.data.set(""),
log("Form cleared")
]
})
addGlobalKeys(app, {
(KeyCode.Z, 'ctrl'): undo(),
(KeyCode.Z, 'ctrl', 'shift'): redo(),
(KeyCode.S, 'ctrl', 'shift'): save_as()
})
from dars.all import *
app = App("Global Shortcuts Example")
docState = State("document",
content="",
saved=False,
message="Ready"
)
@route("/")
def index():
return Page(
Container(
Text("Document Editor", style="text-2xl font-bold"),
Text(useDynamic("document.message"), style="text-slate-500"),
Input(
id="editor",
placeholder="Start typing...",
style="w-full min-h-[200px]"
),
Container(
style="mt-5 p-4 bg-slate-100",
children=[
Text("Global Shortcuts:", style="font-bold"),
Text("• Ctrl+S - Save"),
Text("• Ctrl+Z - Undo"),
Text("• Ctrl+Shift+Z - Redo"),
]
)
)
)
# Global keyboard shortcuts
addGlobalKeys(app, {
(KeyCode.S, 'ctrl'): [
docState.saved.set(True),
docState.message.set("Document saved!"),
alert("Saved!")
],
(KeyCode.Z, 'ctrl'): [
docState.message.set("Undo"),
log("Undo action")
]
})
app.add_page("index", index(), index=True)
if __name__ == "__main__":
app.rTimeCompile()
Bad - Blocks typing:
addGlobalKeys(app, {
KeyCode.ENTER: submit_form() # Blocks Enter in all inputs!
})
Good - Doesn't interfere:
addGlobalKeys(app, {
(KeyCode.ENTER, 'ctrl'): submit_form() # Only Ctrl+Enter
})
For specific components:
Input(
id="search",
on_key_press=onKey(KeyCode.ENTER, perform_search())
)
For app-wide shortcuts:
addGlobalKeys(app, {
(KeyCode.F, 'ctrl'): focus("search")
})
Bad - Repetitive:
Input(
on_key_press=onKey(KeyCode.ENTER, action1())
)
Input(
on_key_press=onKey(KeyCode.ESCAPE, action2())
)
Good - Clean:
Container(
on_key_press=switch({
KeyCode.ENTER: action1(),
KeyCode.ESCAPE: action2()
})
)
formState = State("form", username="")
Input(
id="username",
on_key_press=onKey(KeyCode.ENTER, formState.username.set(V("#username")))
)
on_key_press
for all keyboard events (replaces
on_key_down
and
on_key_up
)
KeyCode
constants for readable key references
onKey()
for single key detection with optional modifiers
switch()
for handling multiple different keys
addGlobalKeys()
for app-wide shortcuts (always with modifiers!)
Exporters are the heart of Dars that allow transforming applications written in Python to different technologies and platforms. Each exporter translates Dars components, styles, and scripts to the native code of the target platform.
All exporters inherit from the base
Exporter
class:
from abc import ABC, abstractmethod
class Exporter(ABC):
def __init__(self):
self.templates_path = "templates/"
@abstractmethod
def export(self, app: App, output_path: str) -> bool:
"""Exports the application to the specific format"""
pass
@abstractmethod
def render_component(self, component: Component) -> str:
"""Renders an individual component"""
pass
@abstractmethod
def get_platform(self) -> str:
"""Returns the name of the platform"""
pass
The HTML exporter generates standard web applications that can run in any browser.
dars export my_app.py --format html --output ./dist
dist/
├── index.html # Main page
├── styles.css # CSS styles
└── script.js # JavaScript logic
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Application</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="container_123" class="dars-container" style="display: flex; flex-direction: column; padding: 20px;">
<span id="text_456" class="dars-text" style="font-size: 24px; color: #333;">Hello Dars!</span>
<button id="button_789" class="dars-button" style="background-color: #007bff; color: white;">Click</button>
</div>
<script src="script.js"></script>
</body>
</html>
styles.css
/* Base Dars styles */
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.dars-button {
display: inline-block;
padding: 8px 16px;
border: 1px solid #ccc;
background-color: #f8f9fa;
color: #333;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
}
.dars-button:hover {
background-color: #e9ecef;
}
from dars.exporters.base import Exporter
class MyCustomExporter(Exporter):
def get_platform(self):
return "my_custom_platform"
def export(self, app, output_path):
# Implement custom export logic
return True
def render_component(self, component):
# Implement custom component rendering
return "generated_code"
The desktop exporter allows you to package your Dars app as a native desktop application. This feature is currently in BETA: usable for testing and internal tooling, but not recommended for production deployments yet.
dars dev
with desktop apps is fully supported, but for now it closes and reopen the electron dev app when a file change is detected.
Initialize or update your project for desktop:
dars init --type desktop
# or if you already have a project
dars init --update
Ensure your project config has desktop format:
{
"entry": "main.py",
"format": "desktop",
"outdir": "dist",
"targetPlatform": "auto"
}
Verify optional tooling (Node/Bun, Electron, electron‑builder):
dars doctor --all --yes
Build your desktop app:
dars build
Artifacts will be placed in
dist/
. The desktop source (used for packaging) is emitted to
dist/source-electron/
.
You can access native filesystem functions via the
dars.desktop
module:
from dars.desktop import read_text, write_text, read_file, write_file, list_directory, get_value
from dars.core.state import this
from dars.scripts.dscript import RawJS, dScript
The desktop module provides async file operations that return
dScript
objects, perfect for chaining with
this().state()
:
Reading Text Files
# Simple read - button updates itself with file content using the Arg helper
from dars.scripts.dscript import Arg
read_btn = Button("Load Config",
on_click=read_text("config.txt").then(
this().state(text=Arg)
)
)
# Update another component
display = Text("", id="display")
load_btn = Button("Load Data",
on_click=read_text("data.txt").then(
update_component("display", text=Arg)
)
)
Writing Text Files
save_btn = Button("Save",
on_click=write_text("output.txt", "Hello Dars!").then(
this().state(text="Saved!", style="text-green-500")
)
)
Listing Directories
from dars.desktop import list_directory, get_value
Button("Browse",
on_click=list_directory(".").then(
this().state(id="file-list", html=RawJS("""
value.map(f => {
const icon = f.isDirectory ? '📁' : '📄';
return `<div>${icon} ${f.name}</div>`;
}).join('')
"""))
)
)
Button("Python Files",
on_click=list_directory(".", "*.py").then(
this().state(id="count", text=RawJS("`Found ${Arg.length} files`"))
)
)
Input(id="path", value=".")
Button("List Directory",
on_click=list_directory(get_value("path")).then(
this().state(id="output", html=RawJS("value.map(f => f.name).join('<br>')"))
)
)
list_directory(".", "*", include_size=True)
Binary File Operations
img_data = read_file("image.png")
write_file("output.bin", data_bytes)
Use
dScript.then()
for sequential operations:
# Read → Process → Update → Write
Button("Process File",
on_click=read_text("input.txt")
.then(dScript(code="const processed = value.toUpperCase(); return processed;"))
.then(write_text("output.txt", RawJS("processed")))
.then(this().state(text="Complete!"))
)
All file paths are relative to the app's directory. See
State Management
and
Scripts
for more details on
this()
and chaining.
targetPlatform
:
auto
|
windows
|
linux
|
macos
.
App
instance when available: title, description, author, version.
0.1.0
is used and a warning is shown.
Dars Framework features a dual-layer logic system: Structured DAP Actions for high-security, high-performance interactions, and JavaScript Scripts for complex client-side logic.
DAP is the modern backbone of Dars interactivity. It replaces raw JavaScript strings with structured data objects that describe actions.
eval()
or
new Function()
calls. This enables a strict Content Security Policy (CSP).
The
dScript
class is the primary way to define logic in Dars. It supports three modes:
Define actions using the
data
parameter. This is the most secure and modern approach.
from dars.all import dScript
# A structured DAP action
my_action = dScript(data={
"op": "alert",
"args": {"message": "Hello from DAP!"}
})
For logic that DAP cannot yet express, you can use raw JS. Dars will attempt to convert this to DAP at compile-time.
script = dScript(code="console.log('Legacy JS');")
Load complex JS modules from your project files.
script = dScript(file_path="./scripts/my_module.js")
When you pass a string to a DAP helper (like
this().state()
), Dars treats it as a text literal. Use
RawJS
to tell the compiler: "This is a JavaScript variable/expression".
from dars.scripts.dscript import RawJS
from dars.core.state import this
# Updates the component text with the value of a JS variable named 'myVar'
update_op = this().state(text=RawJS("myVar"))
When chaining scripts with
.then()
, the result of the previous script is passed to the next one. Use the
Arg
helper to access this value Pythonically.
Arg
is a singleton instance of
_ArgHelper
(a
RawJS
subclass).
from dars.scripts.dscript import Arg
from dars.desktop import read_text
# Read file -> Update component with result
read_op = read_text("data.txt")
update_op = this().state(text=Arg) # Accesses the entire result
update_op_nested = this().state(text=Arg.content) # Accesses 'result.content'
chained = read_op.then(update_op)
.then()
)
All
dScript
and
RawJS
objects support the
.then()
method for sequential execution. This creates an asynchronous pipeline where values flow between steps.
from dars.all import *
action = (
alert("Starting process...")
.then(log("Process step 1"))
.then(alert("Finished!"))
)
utils_ds
)
Dars provides high-level Python helpers that return pre-configured, DAP-compatible
dScript
objects.
| Function | Description |
|---|---|
alert(msg)
|
Shows a browser alert dialog. |
log(msg)
|
Logs a message to the browser console. |
goTo(url)
|
Navigates to a new URL in the same tab. |
goToNew(url)
|
Opens a URL in a new browser tab. |
reload()
|
Reloads the current page. |
show(id)
|
Makes an element visible (
display: block
).
|
hide(id)
|
Hides an element (
display: none
).
|
toggle(id)
|
Toggles an element's visibility. |
setText(id, text)
|
Sets the text content of an element. |
addClass(id, name)
|
Adds a CSS class to an element. |
removeClass(id, name)
|
Removes a CSS class. |
toggleClass(id, name)
|
Toggles a CSS class. |
copyToClipboard(text)
|
Copies the provided text to the system clipboard. |
Animations in Dars return
dScript
objects and can be combined using
sequence()
or chained with
.then()
.
from dars.all import fadeIn, pulse, sequence
# Animate an element when clicked
btn = Button("Animate", on_click=sequence(
fadeIn("box", duration=300),
pulse("box", scale=1.2)
))
| Function | Purpose | Key Parameters |
|---|---|---|
fadeIn
|
Fade element in |
id
,
duration
,
easing
|
fadeOut
|
Fade element out |
id
,
duration
,
hide_after
|
slideIn
|
Slide element in |
id
,
direction
,
duration
|
slideOut
|
Slide element out |
id
,
direction
,
duration
|
scaleIn
|
Scale element in |
id
,
from_scale
,
duration
|
shake
|
Shake effect |
id
,
intensity
,
duration
|
pulse
|
Pulse/heartbeat |
id
,
scale
,
iterations
|
rotate
|
Rotate element |
id
,
degrees
,
duration
|
flip
|
Flip on axis |
id
,
axis
,
duration
|
alert()
,
show()
,
this().state()
) instead of raw JS strings.
RawJS
when you explicitly need to reference a client-side JavaScript variable or expression.
.then()
to create logical sequences, keeping each step focused.
Arg
helper to handle results from asynchronous operations (like API calls or file reading) without writing JavaScript.