Dars Framework is a full-stack Python UI framework for building web 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), and Server-Side Rendered (SSR) applications with FastAPI.
If you are new to Dars, we recommend following the learning path in this order:
dars
command to init, build, and export projects.
V()
,
setVRef()
,
useVRef()
, and
updateVRef()
for modern component-level behavior.
useDynamic
,
useValue
, and other hooks for modern component behavior.
dars.config.json
and environmental variables.
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.
Dars exports web applications as optimized HTML, CSS, and JavaScript. Run
dars build
to generate production-ready web assets in your configured output directory.
dars build
Notes:
dars.config.json
is configured correctly for web export.
dars preview
to inspect the generated output locally.
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
# 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
# New in v1.9.6: Path is optional (defaults to config outdir or ./dist)
dars preview
# New in v1.9.6: Custom port support
dars preview -p 9000
# Build using project config (dars.config.json)
dars build
# Build without the default Python minifier
dars build --no-minify
# Help
dars --help
# Version
dars -v
Scaffold new components and pages instantly with automatic project integration.
# Generate a new FunctionComponent
dars generate component MyComponent
dars g component MyComponent
# Generate a new Page
dars generate page About
# Specify page type directly
dars generate page About --page-type spa
dars generate page Home --page-type static
dars generate page Profile --page-type ssr
# Generate with auto-injection to main.py
dars generate page Contact -y
| 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
|
Preview exported app (auto-detects output) |
dars preview --port 9000
|
Preview on a custom port |
dars build
|
Build using dars.config.json |
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 --port 9000
|
Run the dev server on a custom port |
dars dev --backend
|
Deprecated: use
dars dev
with backendEntry configured.
|
dars info my_app.py
|
Show info about your app |
dars formats
|
List supported export formats |
dars generate component <name>
|
Scaffold a new FunctionComponent |
dars generate page <name>
|
Scaffold a new page (static or SPA) |
dars generate page <name> -y
|
Scaffold and auto-inject into main app |
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 {file.py} --format html --output ./hello_output
dars export {file.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>
.
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
(or the one set in
dars.config.json
) using
app.rTimeCompile()
.
dars dev --backend
)
: Deprecated. Use
dars dev
instead when
backendEntry
is configured; it starts frontend and backend together.
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
|
Deprecated: use
dars dev
with backendEntry configured.
|
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,
"minify": true,
"markdownHighlight": true,
"markdownHighlightTheme": "auto",
"port": 8000,
"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).
outdir
(string): The directory where compiled assets will be saved. Defaults to
dist
. This directory is also the default path used by
dars preview
.
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 now uses dars-bundler — a standalone, high-performance Rust binary — as its sole minification engine. It replaces the previous Python-based rjsmin/rcssmin pipeline and the optional Vite/esbuild dependency.
minify
(boolean, default
true
): Enables or disables JS and CSS minification via
dars-bundler
. When enabled, the bundler runs
SWC
(Rust) for AST-level JS minification with dead-code elimination, and
LightningCSS
for CSS minification.
No Node.js, npm, or Vite installation is required.
bundle
(boolean): Ensures all internal dependencies are bundled into the final distribution.
Note: The old
viteMinifyanddefaultMinifykeys are still accepted for backwards compatibility but are ignored —minifyis the single control point.
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.
port
(number): The port for the development preview server. Defaults to
8000
. This port is respected by both
dars dev
and
dars preview
.
dars-bundler
is a standalone Rust binary that Dars ships alongside its runtime. It is automatically detected and invoked during
dars build
/
dars export
—
no manual configuration is needed
.
dars-bundler
is called on the output directory.
lib/
(the Dars runtime:
dars.min.js
,
dap.js
, etc.) are processed with the same pipeline.
The bundler is resolved in this order:
DARS_BUNDLER_PATH
env variable (user override)
PATH
dars/bundler/
subdirectory shipped with the framework
target/debug
or
target/release
(development mode)
Set
"minify": false
in
dars.config.json
or
pass
--no-minify
to the CLI:
dars build --no-minify
dars export --no-minify
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.
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 provides three dedicated components for conditional rendering and list rendering. They work at both compile time (Python booleans and lists) and runtime (VRef expressions from
setVRef
/
useFetch
).
Show
always renders its children
into the DOM. A falsy condition hides the wrapper with
display:none
. When the condition is a VRef expression, the browser runtime shows or hides the wrapper automatically whenever the VRef value changes.
Use
Show
for loading spinners, error banners, and any UI that toggles based on runtime state.
from dars.all import *
is_loading = setVRef(True, ".tasks-loading")
has_error = setVRef(False, ".tasks-error")
# Loading spinner — visible while is_loading is True
Show(
is_loading,
Container(
Spinner(),
Text("Loading tasks…", style="text-gray-500 ml-2"),
style="flex items-center gap-2 mb-4",
),
)
# Error banner — hidden by default, shown when has_error becomes True
Show(
has_error,
Container(
Text("Could not load tasks. Is the backend running?", style="text-red-600"),
style="bg-red-50 border border-red-200 rounded p-3 mb-4",
),
)
Compile-time bool (static hide/show):
Show(condition=False, Text("Hidden at build time"))
# → renders with style="display:none"
Props:
| Prop | Type | Description |
|---|---|---|
condition
|
bool
or
VRefValue
|
Controls visibility |
*children
|
Component
|
One or more child components to render |
id
|
str
|
HTML
id
attribute
|
class_name
|
str
|
CSS class names |
style
|
dict
|
Inline styles |
Each
renders a list of items using a template function. It supports two modes:
list
is unrolled at export time, one element per item.
VRefValue
(from
setVRef
or
useFetch
) emits a
data-dap-each
attribute; the browser re-renders the container whenever the VRef value changes.
from dars.all import *
users = [
{"name": "Alice", "role": "Admin"},
{"name": "Bob", "role": "User"},
]
Each(
items=users,
render=lambda u: Container(
Text(u["name"], style="font-semibold"),
Text(u["role"], style="text-sm text-gray-500"),
style="flex justify-between p-2 border-b",
),
)
When
items
is a
VRefValue
, the render function is called at
export time
with a sentinel dict containing
__item_<field>__
placeholder strings. The resulting HTML template is stored in
data-each-template
. At runtime,
dom_each_render
substitutes real item values for each element in the list.
from dars.all import *
tasks_vref = setVRef([], ".tasks-data") # starts empty, filled by useFetch
def task_item(t):
# t is the sentinel dict at export time → placeholders like "__item_title__"
# t is a real task dict at runtime → actual values
title = t.get("title", "__item_title__") if isinstance(t, dict) else "__item_title__"
item_id = t.get("id", "__item_id__") if isinstance(t, dict) else "__item_id__"
return Container(
Text(title, style="flex: 1 1 0%", class_name="__item_done_class__"),
Text(f"#{item_id}", style="text-xs text-gray-400 ml-2"),
style="flex items-center gap-2 p-2 border rounded mb-1 bg-white shadow-sm",
)
Each(
items=tasks_vref,
render=task_item,
class_name="space-y-1 mb-6 min-h-[40px]",
)
Special placeholders:
| Placeholder | Replaced with |
|---|---|
__item_<field>__
|
The value of
item["field"]
for each item
|
__item_done_class__
|
"line-through text-gray-400"
if
item["done"]
is truthy,
""
otherwise
|
Supported API response shapes
(automatically unwrapped by
dom_each_render
):
| Response shape | Array used |
|---|---|
{"tasks": [...]}
|
tasks
|
{"items": [...]}
|
items
|
{"data": [...]}
|
data
|
{"results": [...]}
|
results
|
[...]
|
used directly |
Props:
| Prop | Type | Description |
|---|---|---|
items
|
list
or
VRefValue
|
Items to render |
render
|
callable(item) -> Component
|
Template function |
item_key
|
str
|
Key field name for each item (default
"id"
)
|
id
|
str
|
HTML
id
attribute
|
class_name
|
str
|
CSS class names |
style
|
dict
|
Inline styles |
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")
Dars provides a powerful, highly optimized Web Animations API-based engine for scroll and viewport-triggered animations. These run autonomously on the client, without
eval()
or string-execution vulnerabilities.
animateOnView(id, keyframes, duration=300, easing="ease", threshold=0.1, ...)
Triggers an animation as soon as an element enters the viewport.
-
keyframes
: A list of dictionaries representing CSS properties (e.g.,
[{"opacity": "0"}, {"opacity": "1"}]
).
-
threshold
: How much of the element must be visible (0.0 to 1.0) before triggering.
-
once
: If
True
, the animation only runs the first time it enters the viewport.
# Fade and slide up when scrolled into view
index.add_script(animateOnView("my-card", [
{"opacity": "0", "transform": "translateY(50px)"},
{"opacity": "1", "transform": "translateY(0)"}
], duration=800, threshold=0.2))
staggerOnView(ids, keyframes, duration=300, stagger_delay=100, threshold=0.1, ...)
Animates a list of elements sequentially with a delay as soon as the
first
element enters the viewport.
-
ids
: List of element IDs.
-
stagger_delay
: Milliseconds to wait between starting each element's animation.
index.add_script(staggerOnView(
["card-1", "card-2", "card-3"],
[{"opacity": "0", "transform": "scale(0.9)"},
{"opacity": "1", "transform": "scale(1)"}],
duration=500, stagger_delay=150
))
scrollProgress(id, property, from_val, to_val, unit="", start=0, end=1)
Ties a CSS property directly to the page's scroll progress (0.0 = top of page, 1.0 = bottom).
-
property
: CSS property to animate (e.g.,
"opacity"
,
"translateY"
).
-
start
/
end
: The scroll progress range where the animation occurs.
# Fades out the hero section as the user scrolls down
index.add_script(scrollProgress("hero-section", "opacity", 1.0, 0.0, start=0, end=0.2))
runOnView(id, code, threshold=0.1, once=True)
Executes arbitrary JavaScript code (via a secure callback mechanism) when an element enters the viewport.
index.add_script(runOnView("trigger-zone", "console.log('User reached the bottom!');"))
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 provides four route types that control rendering strategy and access control:
| Type | Value | Description |
|---|---|---|
RouteType.PUBLIC
|
"public"
|
Client-side rendered, no auth required. Default. |
RouteType.SSR
|
"ssr"
|
Server-side rendered, fetched from backend on navigation. |
RouteType.PRIVATE
|
"private"
|
Requires authentication. Not included in initial bundle. Redirects to login. |
RouteType.PROTECTED
|
"protected"
|
Requires authentication AND specific roles/permissions. |
from dars.core.route_types import RouteType
@route("/") # PUBLIC (default)
def home():
return Page(Text("Home"))
@route("/dashboard", route_type=RouteType.SSR)
def dashboard():
return Page(Text("Dashboard"))
@route("/account", route_type=RouteType.PRIVATE)
def account():
return Page(Text("My Account"))
@route("/admin", route_type=RouteType.PROTECTED, roles=["admin"])
def admin_panel():
return Page(Text("Admin Panel"))
The
@guard
decorator provides fine-grained access control:
from dars.core.routing import guard
@route("/premium")
@guard(requires_auth=True, roles=["premium"], redirect="/pricing")
def premium_page():
return Page(...)
Or inline via the
RouteGuard
class:
from dars.core.route_types import RouteGuard
@route("/enterprise", guard=RouteGuard(
requires_auth=True,
roles=["enterprise"],
redirect="/contact-sales",
))
def enterprise():
return Page(...)
@route("/beta")
@guard(
requires_auth=True,
custom_check=lambda user: user.get("beta_access", False),
)
def beta_page():
return Page(...)
RouteGuard
configuration before loading a route
/login
(or custom
redirect
path)
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
# Runs frontend and backend together when backendEntry is configured
# Uses backendEntry from dars.config.json (by default "backend.api:app")
Note:
dars dev --backendis deprecated in v1.9.14; usedars devinstead.
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": "/"
}
Dars provides a secure, robust, and completely isolated authentication system. Tokens are kept strictly hidden from the VDOM in the browser using HttpOnly and SameSite cookies, rendering XSS theft impossible. It also integrates strict CSRF protection automatically.
from dars.core.auth import requires_auth, requires_role
# Protect any FastAPI route — injects request.state.user
@app.get("/api/protected")
@requires_auth
async def protected_route(request: Request):
return {"user": request.state.user}
# Role-based access control
@app.get("/api/admin")
@requires_role("admin")
async def admin_only(request: Request):
return {"message": "Admin access granted"}
def verify_user(username, password):
if username == "admin" and password == "secret":
return {"id": "1", "username": "admin", "role": "admin"}
return None
@route("/dashboard", route_type=RouteType.SSR)
@requires_auth(verify_credentials_callback=verify_user, secret="super_secret")
async def dashboard(request: Request):
user = request.state.user
return Page(
Text(f"Welcome, {user['username']}!"),
)
For detailed guides and deep dives into Dars security mechanisms, see Authentication & Security .
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
Vercel / Netlify: - Deploy FastAPI backend as serverless function - Serve static files from CDN - Configure environment variables
Dars provides a production-grade, secure-by-default authentication system. Tokens are transported exclusively via HttpOnly cookies — completely isolated from the VDOM/JavaScript context, making XSS-based token theft impossible.
┌─────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌───────────────┐ Cookies (HttpOnly) │
│ │ Dars VDOM │ ── dars_access_token ──► │
│ │ (JS/HTML) │ ── dars_refresh_token ─► │
│ │ │ ── XSRF-TOKEN (read) ──► │
│ │ Cannot read │ │
│ │ token values │ │
│ └───────────────┘ │
└──────────────────────┬────────────────────────┘
│ Auto cookie attach
▼
┌──────────────────────────────────────────────┐
│ Dars Backend (FastAPI) │
│ │
│ /_dars/auth/{id}/login → Issue tokens │
│ /_dars/auth/{id}/me → User data │
│ /_dars/auth/{id}/refresh → Rotate tokens │
│ /_dars/auth/{id}/logout → Revoke session │
│ │
│ @requires_auth → Validates JWT, injects │
│ request.state.user │
└──────────────────────────────────────────────┘
| Cookie | HttpOnly | SameSite | Secure | Max-Age | Purpose |
|---|---|---|---|---|---|
dars_access_token
|
Yes | Strict | Yes | 15 min | Short-lived JWT |
dars_refresh_token
|
Yes | Strict | Yes | 7 days | Long-lived opaque token |
XSRF-TOKEN
|
No | Strict | Yes | 7 days | CSRF protection |
For multi-auth, cookie names are scoped:
dars_access_token_{auth_id}
,
dars_refresh_token_{auth_id}
.
The callback receives
(username, password)
and returns a user dict (must include
"id"
or
"username"
) or
None
:
def verify_user(username, password):
if username == "admin" and password == "secret":
return {"id": "1", "username": "admin", "role": "admin"}
return None
Supports both sync and async callbacks.
from dars.all import *
app = App(title="My Secure App")
app.setup_auth(
verify_credentials_callback=verify_user,
secret="your-secret-key-change-in-production"
)
Use
@requires_auth
below
@route
for SSR/API routes that need authentication. The decorator automatically injects
request.state.user
:
@route("/dashboard", route_type=RouteType.SSR)
@requires_auth
async def dashboard(request: Request):
user = request.state.user # injected by @requires_auth
return Page(
Text(f"Welcome, {user['username']}!"),
)
@route("/login", route_type=RouteType.SSR)
def login_page():
login_form = collect_form(username=V("#username"), password=V("#password"))
validator = FormValidator({"username": [required()], "password": [required()]})
on_success = redirect_after_login("/dashboard")
submit = validator.validated_submit(
url="/_dars/auth/login",
form_data=login_form,
on_success=on_success,
on_error=setText(".error", "Invalid credentials"),
)
return Page(
Container(
Text("Login", style="text-2xl font-bold"),
Input(id="username", placeholder="Username"),
Input(id="password", placeholder="Password", type="password"),
Text("", class_name="error", style="color: red;"),
Button("Login", on_click=submit),
)
)
@requires_auth
Decorator (Per-Route — Recommended)
Place
@requires_auth
directly below
@route
. You can optionally pass
verify_credentials_callback
and
secret
to auto-register a custom auth scheme:
def verify_guest(username, password):
if username == "guest" and password == "guest":
return {"id": "g1", "username": "guest", "role": "guest"}
return None
@route("/about", route_type=RouteType.SSR)
@requires_auth(verify_credentials_callback=verify_guest, secret="about_secret")
def about():
# Auto-registers auth config with auth_id = "auth_about"
# Endpoints: /_dars/auth/auth_about/login, /me, /logout, /refresh
return Page(...)
When used without arguments (
@requires_auth
as bare decorator), it uses the
default
auth configuration (configured via
app.setup_auth()
).
app.setup_auth()
(Global)
Register one or more global auth configurations:
app = App(title="My App")
app.setup_auth(verify_credentials_callback=verify_admin, secret="admin_secret", auth_id="admin")
app.setup_auth(verify_credentials_callback=verify_user, secret="user_secret", auth_id="default")
register_auth_config()
(Programmatic)
For advanced use cases, register directly:
from dars.backend.auth_routes import register_auth_config
register_auth_config(verify_callback, secret, auth_id="custom")
Dars provides two layers of route protection:
The
@route
decorator supports
RouteType
for client-side routing guards:
from dars.core.route_types import RouteType
# Public — no auth required (default)
@route("/")
# SSR — server-side rendered, no auth
@route("/blog", route_type=RouteType.SSR)
# Private — requires authentication, redirects to login if not auth'd
@route("/account", route_type=RouteType.PRIVATE)
# Protected — requires auth + specific role
@route("/admin", route_type=RouteType.PROTECTED, roles=["admin"])
These guards work at the SPA router level — unauthenticated users are redirected to
/login
(or custom
redirect
path) without the protected component ever loading.
@requires_auth
(Server-Side Enforcement)
For SSR routes and API endpoints, the
@requires_auth
decorator validates the JWT on every request:
from dars.core.auth import requires_auth, requires_role
# Protect any FastAPI route
@app.get("/api/protected")
@requires_auth
async def protected_route(request: Request):
return {"user": request.state.user}
# Role-based access control
@app.get("/api/admin")
@requires_role("admin")
async def admin_only(request: Request):
return {"message": "Admin access granted"}
For maximum security, combine client-side guards with server-side enforcement:
@route("/admin", route_type=RouteType.PROTECTED, roles=["admin"])
@requires_auth(verify_credentials_callback=verify_admin, secret="admin_secret")
async def admin_panel(request: Request):
user = request.state.user
return Page(...)
Dars supports running multiple independent authentication schemes simultaneously, each with its own:
auth_id
suffix)
| Auth ID | Access Cookie | Refresh Cookie |
|---|---|---|
"default"
|
dars_access_token
|
dars_refresh_token
|
"admin"
|
dars_access_token_admin
|
dars_refresh_token_admin
|
"auth_about"
|
dars_access_token_auth_about
|
dars_refresh_token_auth_about
|
A user can be simultaneously logged in with different identities in different sections of the same app without any session collision.
Since the frontend compiles to pure HTML/CSS/JS, you use Dars native APIs to interact with the auth system:
fetch_me, *_ = useFetch(
"/_dars/auth/me",
method="GET",
on_success=runSequence(
updateVRef(".show-dashboard", True),
updateVRefFromResponse(".user-name", key="response.user.username"),
),
on_error=runSequence(updateVRef(".show-login", True)),
)
page.add_script(fetch_me)
For multi-auth, scope the endpoint:
fetch_me, *_ = useFetch("/_dars/auth/admin/me", method="GET", ...)
login_form = collect_form(username=V("#username"), password=V("#password"))
validator = FormValidator({"username": [required()], "password": [required()]})
submit = validator.validated_submit(
url="/_dars/auth/login",
form_data=login_form,
on_success=redirect_after_login("/dashboard"),
on_error=runSequence(updateVRefFromResponse(".error", key="response.detail")),
)
fetch_logout, *_ = useFetch("/_dars/auth/logout", method="POST",
on_success=runSequence(updateVRef(".show-dashboard", False), updateVRef(".show-login", True)))
Validates JWT on every request (except excluded paths). Injects
request.state.user
:
from dars.backend.middleware import AuthMiddleware
app.add_middleware(
AuthMiddleware,
secret="your-secret",
exclude_paths=["/_dars/auth", "/api/public", "/docs"],
csrf_protection=True,
)
Injects security headers into every response:
from dars.backend.middleware import SecurityHeadersMiddleware
app.add_middleware(
SecurityHeadersMiddleware,
csp="default-src 'self'",
hsts=True,
)
Default headers:
X-Content-Type-Options
,
X-Frame-Options
,
X-XSS-Protection
,
Referrer-Policy
,
Permissions-Policy
.
DarsAuth.encode_token(payload, secret, algorithm="HS256", expires_in=3600)
JWT encoding with HMAC-SHA256. Returns token string.
DarsAuth.decode_token(token, secret, algorithm="HS256")
Validate JWT, return payload dict. Raises
ValueError
on failure/expiry.
DarsAuth.hash_password(password)
/
DarsAuth.verify_password(password, hashed)
PBKDF2-SHA256 password hashing (100k iterations) with timing-attack resistant comparison.
DarsAuth.set_auth_cookies(response, access_token, refresh_token, xsrf_token, auth_id="default")
Sets HttpOnly secure cookies with scoped names.
DarsAuth.clear_auth_cookies(response, auth_id="default")
Clears all auth cookies.
@requires_auth(verify_credentials_callback=None, secret=None, auth_id=None)
Decorator for FastAPI route handlers. Validates JWT, injects
request.state.user
. Auto-registers auth config when callback and secret are provided.
@requires_role(role)
Decorator for role-based access control. Requires
@requires_auth
or middleware to have set
request.state.user
first.
app.setup_auth(verify_credentials_callback, secret, auth_id="default", login_page="/login")
Register a global auth configuration.
register_auth_config(verify_credentials_callback, secret, auth_id)
Programmatic auth config registration.
get_auth_config(auth_id)
Retrieve an auth configuration by ID.
When auth is configured, Dars automatically exposes:
| Method | Path | Description |
|---|---|---|
POST
|
/_dars/auth/{auth_id}/login
|
Validate credentials, issue session |
POST
|
/_dars/auth/{auth_id}/refresh
|
Rotate tokens using refresh token |
POST
|
/_dars/auth/{auth_id}/logout
|
Revoke session, clear cookies |
GET
|
/_dars/auth/{auth_id}/me
|
Return authenticated user data |
For the default scheme (
auth_id="default"
), the path simplifies to
/_dars/auth/login
, etc.
from dars.backend.session import SessionManager, InMemorySessionStore
store = InMemorySessionStore()
manager = SessionManager(store)
token = manager.issue_refresh_token(user_id, payload)
session = manager.validate_refresh_token(token)
manager.revoke_refresh_token(token)
The
SessionStore
protocol allows custom implementations for Redis, database-backed sessions, etc.
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:
createComp()
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
useData('userData').name
accesses nested properties
.then()
chaining
- Chain multiple state updates sequentially
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:
dScript.ARG
to reference values from previous scripts
from dars.scripts.dscript import RawJS, Arg
# Example: update component with the result from a previous action
update_op = this().state(text=Arg)
chained = some_async_action.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.
The reactivity system in Dars is divided into the following sections:
V()
helper and expression system used throughout all hooks.
State
objects for shared application logic.
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")
V()
also supports extracting values directly from reactive state created by
useDynamic()
(covered in detail in the
Global Application State
section below):
# 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")
textContent
value
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:
+
,
-
,
*
,
/
,
%
,
**
)
.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())
**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()
The VRef system allows you to define and manage state declaratively without creating complex global objects. It is the recommended approach for local component state.
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).
#id
for single elements.
.class
for multiple elements sharing the value.
Returns:
VRefValue
: An object representing the value, ready to be passed to components.
The
useVRef()
hook allows you to create reactive bindings between
setVRef
values and your UI. It seamlessly works with the
V()
value extractor and mathematical expressions.
Use it to wrap
V()
expressions directly into component properties:
from dars.all import *
# Define the state globally or locally
price = setVRef(10.0, ".item-price")
# Consume the state reactively
Container(
Text("Price: $"),
Text(text=useVRef(V(".item-price")))
)
useVRef()
truly shines when combined with mathematical and boolean operations. It automatically tracks dependencies and updates the UI instantly:
# Create basic states
setVRef(19.99, ".price")
setVRef(2, ".quantity")
# Reactive mathematical expression
Text(
text=useVRef(V(".price").float() * V(".quantity").int())
)
# Reactive boolean expressions
Button(
"Checkout",
disabled=useVRef(V(".quantity").int() == 0)
)
You do
not
need to specify which selectors to watch. The compiler automatically walks the
V()
expression tree and extracts every CSS selector used. At runtime, whenever any of those selectors is updated via
updateVRef()
, the binding re-evaluates and patches the DOM.
# The framework auto-detects that this binding depends on ".price" and ".qty"
Text(text=useVRef(V(".price").float() * V(".qty").int()))
# When either changes, the text updates automatically:
Button("Double price", on_click=updateVRef(".price", V(".price").float() * 2))
Button("+1 qty", on_click=updateVRef(".qty", V(".qty").int() + 1))
If you need to override auto-detection, pass explicit selectors via the
dependencies
parameter:
Text(text=useVRef(
V(".total").float(),
dependencies=[V(".price"), V(".qty")] # explicit override
))
Pass
dScript
,
RawJS
, or plain strings via the
callbacks
parameter. They fire every time the binding re-evaluates, enabling side-effects like logging or chained updates.
from dars.scripts.dscript import RawJS
# Single callback (no list needed)
Text(text=useVRef(
V(".value-stuff"),
callbacks=RawJS("console.log('value-stuff changed!');")
))
# dScript callback
Text(text=useVRef(
V(".score").int(),
callbacks=log("Score was updated")
))
# Multiple callbacks (list)
Text(text=useVRef(
V(".total").float(),
callbacks=[
log("total changed"),
RawJS("document.title = 'Total: ' + document.querySelector('.total-display').textContent;")
]
))
useVRef(
vexpr: Union[ValueRef, MathExpression, BooleanExpression, Any],
dependencies: Optional[List[ValueRef]] = None,
callbacks: Optional[Union[dScript, RawJS, str, List[Union[dScript, RawJS, str]]]] = None
) -> VRefBinding
Parameters:
vexpr
: A
V()
expression, an operator chain, or a direct literal value.
dependencies
: Optional explicit list of
V()
selectors to watch. If omitted, selectors are auto-detected from the expression tree.
callbacks
: Optional callback(s) fired every time the binding re-evaluates. Accepts a single
dScript
,
RawJS
, or
str
, or a list of those.
Returns:
VRefBinding
: An object that resolves to the current value on render, and injects JavaScript to update reactively on hydration.
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:
V("#input")
- Extract values
V("#input").length() >= 3
- Boolean validation
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:
"text"
,
42
,
True
V("#source")
V("#input").upper()
V("#a").int() + V("#b").int()
(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
.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:
Use
State.set()
when:
Use both (Hybrid):
For state that needs to be shared across many disconnected components or persists across navigation, use
State
objects.
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
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:
useValue("user.name", ".name-input")
sets initial value "Jane Doe" and applies class
name-input
to the input
V(".name-input")
extracts the current value (even if modified by user)
Supported selectors:
.foo
) → Added to element's
class
attribute
#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
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:
"user.name"
)
["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")
)
productState.info.set(...)
)
dScript
Behavior:
V()
helper
[!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"
)
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())
Declarative form validation with dual client/server enforcement.
| Constructor | Description |
|---|---|
required()
|
Field must be non-empty |
min_length(n)
|
Minimum character count |
max_length(n)
|
Maximum character count |
email()
|
Must be a valid email address |
pattern(regex)
|
Must match regex |
min_value(n)
|
Numeric minimum |
max_value(n)
|
Numeric maximum |
custom(fn)
|
Python callable
(value) -> Optional[str]
|
validated_submit
Validates all rules client-side first. Only fires the network request if every rule passes. Error messages appear in
#{field}-error
elements.
task_form = collect_form(title=V("#title"))
validator = FormValidator({
"title": [required(), min_length(3), max_length(100)],
})
submit_action = validator.validated_submit(
url="/api/tasks",
form_data=task_form,
on_success=runSequence(clearInput("title"), fetch_trigger),
on_error=setText("submit-error", "Error submitting. Try again."),
)
# In your Page:
Input(id="title", placeholder="Task title…"),
Text("", id="title-error", style="text-red-500 text-sm"),
Button("Add Task", on_click=submit_action),
Important: The input
idmust match the field name inFormValidatorso the selector#titleresolves correctly.
errors = validator.validate_server({"title": "Hi"})
# → {"title": ["Must be at least 3 characters."]}
errors = validator.validate_server({"title": "Hello World"})
# → {} (all pass)
useFetch
is the primary hook for fetching data from APIs and binding the response to reactive VRefs. It returns a 4-tuple of
(trigger, loading_vref, data_vref, error_vref)
— all pure Python objects.
from dars.all import *
trigger, loading, data, error = useFetch("/api/users")
page = Page(
Show(loading, Spinner()),
Show(error, Text("Error loading data", style="text-red-500")),
Button("Reload", on_click=trigger),
)
page.add_script(trigger) # auto-run on page load
tasks_sel = ".tasks-data"
_tasks = setVRef([], tasks_sel)
trigger, loading, _, error = useFetch(
"/api/tasks",
on_success=runSequence(
updateVRef(".loading", False),
updateVRefFromResponse(tasks_sel), # store response → VRef
),
on_error=runSequence(
updateVRef(".loading", False),
updateVRef(".error", True),
),
)
trigger, loading, data, error = useFetch(
"/api/search",
method="POST",
body={"query": "dars"},
headers={"Content-Type": "application/json"},
)
useFetch(
url: str,
method: str = "GET",
body: Any = None,
headers: dict = None,
on_success: dScript = None,
on_error: dScript = None,
) -> Tuple[dScript, VRefValue, VRefValue, VRefValue]
Returns:
(trigger_script, loading_vref, data_vref, error_vref)
trigger_script
—
dScript
that initiates the fetch. Use as
on_click
handler or in
runSequence
, or pass to
page.add_script()
to auto-run on load.
loading_vref
—
VRefValue
that is
True
while the request is in flight.
data_vref
—
VRefValue
that holds the parsed response on success.
error_vref
—
VRefValue
that holds the error message on failure.
Stores the API response from a
useFetch
on_success
context into a VRef selector. The
network_request
DAP op passes the parsed response as
ctx.response
.
updateVRefFromResponse(selector: str, key: str = "response") -> dScript
on_success=runSequence(
updateVRef(".loading", False),
updateVRefFromResponse(".tasks-data"), # stores ctx.response → VRef
)
You can use dot-notation in the
key
parameter to drill down into nested JSON response structures (e.g.
response.user.username
,
response.data.items
). This makes Dars completely compatible with any backend or third-party JSON API, extracting just the data you need without manual transformation.
on_success=runSequence(
updateVRefFromResponse(".user-name", key="response.user.username"),
updateVRefFromResponse(".user-role", key="response.user.role"),
)
Execute multiple
dScript
/
RawJS
actions in sequence. Accepts any mix of hooks, VRef updates, fetch triggers, and DAP actions.
from dars.all import *
Button("Submit",
on_click=runSequence(
updateVRef(".loading", True),
fetch_trigger,
clearInput("my-input"),
)
)
Do:
useDynamic
for simple text/value updates.
useWatch
for side effects like logging, analytics, or complex logic.
useValue
with selectors for form inputs that need value extraction.
"user"
,
"cart"
).
.int()
or
.float()
before arithmetic operations with
V()
.
Don't:
stateName.property
).
Dars Action Protocol (DAP) is the secure command system that powers all client-side interactivity. Every event handler, animation, and state mutation compiles down to structured DAP payloads — never
eval()
or
new Function()
.
| Operation | Description | Python Helper |
|---|---|---|
navigate
|
Navigate to path |
goTo(path)
|
navigate_new
|
Open in new tab |
goToNew(path)
|
reload
|
Reload page |
reload()
|
history_back
|
Go back |
historyBack()
|
history_forward
|
Go forward |
historyForward()
|
| Operation | Description | Python Helper |
|---|---|---|
change
|
Change state value |
state.count.set(n)
|
| Operation | Description | Python Helper |
|---|---|---|
modal_show
|
Show modal |
showModal(id)
|
modal_hide
|
Hide modal |
hideModal(id)
|
| Operation | Description | Python Helper |
|---|---|---|
dom_show
|
Show element |
show(id)
|
dom_hide
|
Hide element |
hide(id)
|
dom_toggle
|
Toggle visibility |
toggle(id)
|
| Operation | Description | Python Helper |
|---|---|---|
dom_set_text
|
Set text content |
setText(id, text)
|
dom_set_html
|
Set inner HTML (sanitized) |
setHTML(id, html)
|
dom_set_style
|
Set inline style |
setStyle(id, styles)
|
dom_set_value
|
Set form value |
setValue(id, val)
|
dom_set_attr
|
Set attribute |
setAttr(id, name, val)
|
dom_reflow
|
Force browser reflow |
reflow(id)
|
dom_animate
|
Animate element |
animate(id, keyframes)
|
| Operation | Description | Python Helper |
|---|---|---|
class_add
|
Add CSS class |
addClass(id, cls)
|
class_remove
|
Remove CSS class |
removeClass(id, cls)
|
class_toggle
|
Toggle CSS class |
toggleClass(id, cls)
|
| Operation | Description | Python Helper |
|---|---|---|
scroll_to
|
Scroll to position |
scrollTo(x, y)
|
scroll_top
|
Scroll to top |
scrollTop()
|
scroll_bottom
|
Scroll to bottom |
scrollBottom()
|
scroll_to_element
|
Scroll element into view |
scrollToElement(id)
|
| Operation | Description | Python Helper |
|---|---|---|
form_submit
|
Submit form |
formSubmit(id)
|
form_reset
|
Reset form |
formReset(id)
|
input_clear
|
Clear input |
clearInput(id)
|
input_set
|
Set input value |
inputSet(id, value)
|
| Operation | Description | Python Helper |
|---|---|---|
clipboard_write
|
Write to clipboard |
copyToClipboard(text)
|
clipboard_copy_element
|
Copy element text |
copyElementText(id)
|
| Operation | Description | Python Helper |
|---|---|---|
sequence
|
Run actions in sequence |
sequence(*actions)
|
delay
|
Delay then run action |
delay(ms, action)
|
conditional
|
Conditionally run action |
condition(expr, then, else)
|
| Operation | Description | Python Helper |
|---|---|---|
vref_update
|
Update VRef value |
updateVRef(sel, val)
|
vref_get
|
Get VRef value | — |
| Operation | Description | Python Helper |
|---|---|---|
comp_create
|
Create component dynamically |
createComp(data, root)
|
comp_delete
|
Delete component |
deleteComp(id)
|
comp_update
|
Update component props |
updateComp(id, props)
|
| Operation | Description | Python Helper |
|---|---|---|
fetch
|
HTTP request |
fetch(url, ...)
|
call_server
|
Call server action |
call_server(name, **kwargs)
|
| Operation | Description | Python Helper |
|---|---|---|
alert
|
Show alert |
alert(msg)
|
confirm
|
Show confirmation |
confirm(msg, on_ok, on_cancel)
|
log
|
Console log |
log(msg)
|
{op, args}
payloads
_dispatch()
function only executes predefined operations
new Function()
dom_set_html
operations sanitize input before DOM injection
Dars provides a complete fullstack toolkit: HTTP utilities, a declarative fetch hook, form validation, file uploads, a JSON store, security middleware, and environment management — all in pure Python.
Dars SSR projects use
SSRApp
to wire together FastAPI, CORS, security headers, and file uploads in one place.
my-app/
├── main.py # Dars frontend (routes, components)
├── backend/
│ ├── api.py # FastAPI entry point
│ └── apiConfig.py # Environment config
├── dars.config.json
└── .env # Environment variables (auto-loaded)
backend/api.py
from dars.backend.ssr import SSRApp
from backend.apiConfig import DarsEnv
from main import app as dars_app
ssr = SSRApp(dars_app, prefix="/api/ssr", title="My App - Backend")
urls = DarsEnv.get_urls()
ssr.use_cors(
origins=[urls["frontend"], "http://127.0.0.1:4000"],
credentials=True,
)
ssr.use_security_headers()
ssr.use_upload(
upload_dir="uploads",
allowed_types=["image/png", "image/jpeg", "application/pdf"],
max_size_bytes=10 * 1024 * 1024,
path="/api/upload",
)
app = ssr.fastapi_app
# ── Production: serve dist/ as static files with SPA fallback ───────────────
if not DarsEnv.is_dev():
ssr.use_spa_fallback()
# ────────────────────────────────────────────────────────────────────────────
# Custom API routes
from fastapi import Request
from fastapi.responses import JSONResponse
from dars.backend.store import JsonStore
_store = JsonStore("tasks_db.json", default={"tasks": []})
@app.get("/api/tasks")
async def get_tasks():
return JSONResponse({"tasks": _store.get("tasks", [])})
@app.post("/api/tasks")
async def create_task(request: Request):
body = await request.json()
tasks = _store.get("tasks", [])
task = {"id": len(tasks) + 1, "title": body.get("title", ""), "done": False}
tasks.append(task)
_store.set("tasks", tasks)
return JSONResponse(task, status_code=201)
if __name__ == "__main__":
import uvicorn
print("\n" + "=" * 60)
print("Dars Fullstack Backend")
print("=" * 60)
if DarsEnv.is_dev():
print(f" Backend: {urls['backend']}")
print(f" Frontend: {urls['frontend']} (run 'dars dev' separately)")
print(f" API Docs: {urls['backend']}/docs")
port, host = 3000, "127.0.0.1"
else:
frontend_dir = DarsEnv.get_frontend_dist_dir()
print(f" App: http://0.0.0.0:8000 (frontend + API, same origin)")
print(f" Frontend: {frontend_dir}")
port, host = 8000, "0.0.0.0"
print("=" * 60 + "\n")
uvicorn.run(app, host=host, port=port)
backend/apiConfig.py
import os
class DarsEnv:
MODE = os.environ.get("DARS_MODE", "development")
@staticmethod
def is_dev():
return DarsEnv.MODE == "development"
@staticmethod
def get_urls():
if DarsEnv.is_dev():
return {"backend": "http://localhost:3000", "frontend": "http://localhost:4000"}
return {"backend": "/", "frontend": "/"}
# Run frontend and backend together in development
# (backendEntry must be configured in dars.config.json)
dars dev
Note:
dars dev --backendis deprecated in v1.9.14. Usedars devto start the fullstack development workflow.
useFetch
creates a fetch trigger and three reactive VRefs (loading, data, error) wired to a
network_request
DAP action. No JavaScript needed.
useFetch(
url: str,
method: str = "GET",
body: Any = None,
headers: dict = None,
on_success: dScript = None,
on_error: dScript = None,
) -> Tuple[dScript, VRefValue, VRefValue, VRefValue]
Returns
(trigger_script, loading_vref, data_vref, error_vref)
.
from dars.all import *
trigger, loading, data_vref, error = useFetch("/api/users")
page = Page(
Show(loading, Spinner()),
Show(error, Text("Error loading data", style="text-red-500")),
Text(data_vref),
Button("Reload", on_click=trigger),
)
page.add_script(trigger) # auto-run on load
tasks_sel = ".tasks-data"
_tasks = setVRef([], tasks_sel)
trigger, loading, _, error = useFetch(
"/api/tasks",
on_success=runSequence(
updateVRef(".loading", False),
updateVRefFromResponse(tasks_sel), # store response → VRef
),
on_error=runSequence(
updateVRef(".loading", False),
updateVRef(".error", True),
),
)
trigger, loading, data, error = useFetch(
"/api/search",
method="POST",
body={"query": "dars"},
headers={"Content-Type": "application/json"},
)
Stores the API response from a
useFetch
on_success
context into a VRef selector. The
network_request
DAP op passes the parsed response as
ctx.response
.
updateVRefFromResponse(selector: str, key: str = "response") -> dScript
on_success=runSequence(
updateVRef(".loading", False),
updateVRefFromResponse(".tasks-data"),
)
You can use dot-notation in the
key
parameter to drill down into nested JSON response structures (e.g.
response.user.username
,
response.data.items
). This makes Dars completely compatible with any backend or third-party JSON API, extracting just the data you need without manual transformation.
on_success=runSequence(
updateVRefFromResponse(".user-name", key="response.user.username"),
updateVRefFromResponse(".user-role", key="response.user.role"),
)
Declarative validation with dual client/server enforcement. Rules are declared once in Python.
| Constructor | Description |
|---|---|
required()
|
Field must be non-empty |
min_length(n)
|
Minimum character count |
max_length(n)
|
Maximum character count |
email()
|
Must be a valid email address |
pattern(regex)
|
Must match regex |
min_value(n)
|
Numeric minimum |
max_value(n)
|
Numeric maximum |
custom(fn)
|
Python callable
(value) -> Optional[str]
|
validated_submit
Validates all rules client-side first. Only fires the network request if every rule passes. Error messages appear in
#{field}-error
elements.
from dars.all import *
task_form = collect_form(title=V("#title"))
validator = FormValidator({
"title": [required(), min_length(3), max_length(100)],
})
submit_action = validator.validated_submit(
url="/api/tasks",
form_data=task_form,
on_success=runSequence(clearInput("title"), fetch_trigger),
on_error=setText("submit-error", "Error submitting. Try again."),
)
# In your Page:
Input(id="title", placeholder="Task title…"),
Text("", id="title-error", style="text-red-500 text-sm"),
Button("Add Task", on_click=submit_action),
Important: The input
idmust match the field name inFormValidatorso the selector#titleresolves correctly.
errors = validator.validate_server({"title": "Hi"})
# → {"title": ["Must be at least 3 characters."]}
errors = validator.validate_server({"title": "Hello World"})
# → {} (all pass)
rules_json = validator.get_rules_json()
# → '{"title": [{"type": "required"}, {"type": "min_length", "n": 3}]}'
Thread-safe, atomic-write JSON persistence. Ideal for prototyping and small backends.
from dars.all import *
store = JsonStore("data.json", default={"tasks": []})
# Read
tasks = store.get("tasks", [])
# Write (atomic)
store.set("tasks", tasks + [{"id": 1, "title": "New task"}])
# Delete key
store.delete("tasks")
# Get all
all_data = store.all()
# Clear
store.clear()
.tmp
file then atomically replaces the target via
os.replace()
threading.Lock
on all mutating operations
ValueError
on malformed JSON with a descriptive message
Server-side file upload handler with MIME type validation, size limits, and filename sanitisation.
from dars.all import *
pipeline = UploadPipeline(
upload_dir="uploads",
allowed_types=["image/png", "image/jpeg", "image/gif", "application/pdf"],
max_size_bytes=10 * 1024 * 1024, # 10 MB
)
pipeline.create_endpoint(app, path="/api/upload")
| Status | Condition |
|---|---|
200
|
Upload successful — returns
{"url": "/uploads/filename.png"}
|
413
|
File exceeds
max_size_bytes
|
415
|
MIME type not in
allowed_types
|
500
|
Disk write failure |
import uuid
pipeline = UploadPipeline(
upload_dir="uploads",
rename_fn=lambda name: f"{uuid.uuid4().hex}_{name}",
)
UploadPipeline.sanitize_filename(filename)
removes
../
,
./
, and any character outside
[a-zA-Z0-9._-]
.
FileUpload(
upload_url="/api/upload",
accepted_types=["image/png", "image/jpeg"],
max_size_bytes=5 * 1024 * 1024,
on_upload_complete=setText("status", "Uploaded!"),
on_upload_error=setText("status", "Upload failed."),
)
Injects HTTP security headers into every response. Headers are only added when not already present, so application code can override any individual header.
from dars.all import *
# Via SSRApp (recommended)
ssr.use_security_headers()
# Or manually
from dars.backend.middleware import SecurityHeadersMiddleware
app.add_middleware(SecurityHeadersMiddleware, csp="default-src 'self'", hsts=True)
Default headers injected:
| Header | Value |
|---|---|
X-Content-Type-Options
|
nosniff
|
X-Frame-Options
|
DENY
|
X-XSS-Protection
|
1; mode=block
|
Referrer-Policy
|
strict-origin-when-cross-origin
|
Permissions-Policy
|
geolocation=(), microphone=(), camera=()
|
Optional:
-
csp="..."
→ adds
Content-Security-Policy
-
hsts=True
→ adds
Strict-Transport-Security: max-age=31536000; includeSubDomains
DarsEnv
loads
.env
files and provides typed access to environment variables.
.env
File
API_KEY=my-secret-key
DATABASE_URL=sqlite:///./app.db
DEBUG=true
from dars.env import DarsEnv
# Load .env (called automatically by load_config)
DarsEnv.load()
DarsEnv.load(path="config/.env") # custom path
# Read
api_key = DarsEnv.get("API_KEY") # None if missing
api_key = DarsEnv.get("API_KEY", "default")
# Require (raises KeyError if missing)
secret = DarsEnv.require("SECRET_KEY")
# Dev/prod mode
DarsEnv.set_dev_mode(True)
if DarsEnv.dev:
print("Running in development mode")
Rules:
- Does not overwrite existing
os.environ
keys
- Strips surrounding single or double quotes from values
- Skips blank lines and
#
comments
- Splits on the first
=
only (values may contain
=
)
- Called automatically by
load_config()
before reading
dars.config.json
Lower-level HTTP helpers that return
dScript
objects for use in event handlers.
from dars.backend.http import fetch, get, post, put, delete, patch
# GET
get_users = get(id="userData", url="/api/users")
# POST
create_user = post(
id="createResult",
url="/api/users",
body={"name": "Alice"},
callback=alert("User created!"),
on_error=alert("Error!"),
)
from dars.backend.http import add_request_interceptor, use_auth_interceptor
# Add auth token to every request
use_auth_interceptor(token_key="dars_auth_token")
# Custom interceptor
add_request_interceptor(lambda cfg: {**cfg, "headers": {**cfg.get("headers", {}), "X-App": "1"}})
useData
— Access Response Data
from dars.backend.data import useData
# Dot-notation access to fetched data
name_state.text.set(useData("userData").name)
city_state.text.set(useData("userData").address.city)
Create, update, and delete components dynamically at runtime.
from dars.all import *
new_item = Text("Hello!", id="new-item")
Button("Add", on_click=createComp(new_item, root="container-id", position="append"))
Button("Remove", on_click=deleteComp("new-item"))
Button("Update", on_click=updateComp("new-item", text="Updated!"))
A complete task manager using
useFetch
,
Each
,
FormValidator
,
JsonStore
, and
SSRApp
.
pages/did.py
)
from dars.all import *
@route("/did")
def did():
loading_sel = ".tasks-loading"
error_sel = ".tasks-error"
tasks_sel = ".tasks-data"
is_loading = setVRef(True, loading_sel)
has_error = setVRef(False, error_sel)
_tasks = setVRef([], tasks_sel)
on_fetch_success = runSequence(
updateVRef(loading_sel, False),
updateVRef(error_sel, False),
updateVRefFromResponse(tasks_sel),
)
on_fetch_error = runSequence(
updateVRef(loading_sel, False),
updateVRef(error_sel, True),
)
fetch_trigger, _lv, _dv, _ev = useFetch(
"/api/tasks",
on_success=on_fetch_success,
on_error=on_fetch_error,
)
task_form = collect_form(title=V("#title"))
validator = FormValidator({"title": [required(), min_length(3), max_length(100)]})
submit_action = validator.validated_submit(
url="/api/tasks",
form_data=task_form,
on_success=runSequence(clearInput("title"), fetch_trigger),
on_error=setText("submit-error", "Error submitting. Try again."),
)
def task_item(t):
title = t.get("title", "__item_title__") if isinstance(t, dict) else "__item_title__"
item_id = t.get("id", "__item_id__") if isinstance(t, dict) else "__item_id__"
return Container(
Text(title, style="flex: 1 1 0%", class_name="__item_done_class__"),
Text(f"#{item_id}", style="text-xs text-gray-400 ml-2"),
style="flex items-center gap-2 p-2 border rounded mb-1 bg-white shadow-sm",
)
page = Page(
Container(
Head("Task Manager"),
Text("Task Manager", style="text-3xl font-bold mb-1 text-indigo-600"),
Show(is_loading, Container(Spinner(), Text("Loading…"), style="flex gap-2 mb-4")),
Show(has_error, Container(Text("Backend not running?", style="text-red-600"),
style="bg-red-50 border rounded p-3 mb-4")),
Each(items=_tasks, render=task_item, class_name="space-y-1 mb-6 min-h-[40px]"),
Container(
Text("Add a task", style="font-semibold mb-2"),
Input(id="title", placeholder="Task title (min 3 chars)…",
class_name="border rounded px-3 py-2 w-full mb-1"),
Text("", id="title-error", style="text-red-500 text-sm mb-1"),
Text("", id="submit-error", style="text-red-500 text-sm mb-2"),
Button("Add Task", on_click=submit_action,
style="bg-indigo-600 text-white px-4 py-2 rounded"),
style="bg-white border rounded-xl p-4 shadow-sm mb-4",
),
Button("↻ Refresh", on_click=fetch_trigger,
style="text-sm text-indigo-500 underline"),
style="max-w-xl mx-auto p-8 font-sans",
)
)
page.add_script(fetch_trigger)
return page
backend/api.py
)
from dars.backend.ssr import SSRApp
from backend.apiConfig import DarsEnv
from main import app as dars_app
from dars.backend.store import JsonStore
from fastapi import Request
from fastapi.responses import JSONResponse
import os
ssr = SSRApp(dars_app, prefix="/api/ssr")
urls = DarsEnv.get_urls()
ssr.use_cors(origins=[urls["frontend"]], credentials=True)
ssr.use_security_headers()
app = ssr.fastapi_app
# ── Production: serve dist/ as static files with SPA fallback ───────────────
if not DarsEnv.is_dev():
ssr.use_spa_fallback()
# ────────────────────────────────────────────────────────────────────────────
_store = JsonStore(
path=os.path.join(os.path.dirname(__file__), "..", "tasks_db.json"),
default={"tasks": []},
)
@app.get("/api/tasks")
async def get_tasks():
return JSONResponse({"tasks": _store.get("tasks", [])})
@app.post("/api/tasks")
async def create_task(request: Request):
body = await request.json()
tasks = _store.get("tasks", [])
task = {"id": len(tasks) + 1, "title": body.get("title", ""), "done": False}
tasks.append(task)
_store.set("tasks", tasks)
return JSONResponse(task, status_code=201)
if __name__ == "__main__":
import uvicorn
print("\n" + "=" * 60)
print("Dars Fullstack Backend")
print("=" * 60)
if DarsEnv.is_dev():
port, host = 3000, "127.0.0.1"
else:
port, host = 8000, "0.0.0.0"
print("=" * 60 + "\n")
uvicorn.run(app, host=host, port=port)
fetch_trigger
fires →
network_request
DAP op hits
/api/tasks
JsonStore
→ returns
{"tasks": [...]}
on_success
runs →
updateVRefFromResponse(".tasks-data")
stores response in VRef
dom_each_render
detects VRef change → substitutes
__item_title__
,
__item_id__
placeholders → list renders
#title
input → clicks "Add Task"
FormValidator
checks
required()
+
min_length(3)
client-side
network_request
POSTs to
/api/tasks
→ backend appends to
JsonStore
on_success
→
clearInput("title")
+
fetch_trigger
→ list refreshes
useFetch(url, method, body, headers, on_success, on_error)
Returns
(trigger, loading_vref, data_vref, error_vref)
.
updateVRefFromResponse(selector, key="response")
Stores
ctx[key]
from fetch context into a VRef.
FormValidator(fields)
.validated_submit(url, form_data, on_success, on_error)
— validate then submit
.validate_server(data)
→
dict
of errors
.get_rules_json()
→ JSON string
JsonStore(path, default)
.get(key, default)
,
.set(key, value)
,
.delete(key)
,
.all()
,
.clear()
UploadPipeline(upload_dir, allowed_types, max_size_bytes, rename_fn)
.create_endpoint(app, path)
— registers FastAPI POST endpoint
.sanitize_filename(filename)
— static method
SecurityHeadersMiddleware(app, csp, hsts)
Starlette middleware. Use via
ssr.use_security_headers()
or
app.add_middleware(...)
.
DarsEnv
.load(path=".env")
,
.get(key, default)
,
.require(key)
,
.set_dev_mode(bool)
Dars ships with a built-in declarative ORM for SQLite, featuring auto-generated CRUD API endpoints.
from dars.backend.models import DarsModel, TextField, IntegerField, BooleanField, FloatField, DateTimeField, JSONField, ForeignKey
from dars.backend.database import Database
class Product(DarsModel):
__tablename__ = "products"
name = TextField(nullable=False)
price = IntegerField(default=0)
in_stock = BooleanField(default=True)
class Order(DarsModel):
__tablename__ = "orders"
product = ForeignKey(Product)
quantity = IntegerField(default=1)
| Field | SQL Type | Description |
|---|---|---|
TextField()
|
TEXT
|
String values |
IntegerField()
|
INTEGER
|
Integer values |
FloatField()
|
REAL
|
Float values |
BooleanField()
|
INTEGER
(0/1)
|
Boolean values |
DateTimeField()
|
TEXT
(ISO 8601)
|
Datetime values (
auto_now=True
for timestamps)
|
JSONField()
|
TEXT
(JSON)
|
JSON-serializable data |
ForeignKey(model)
|
INTEGER
|
Foreign key reference |
from dars.backend.database import Database
db = Database("app.db") # File-based
db = Database(":memory:") # In-memory (default)
db.register(Product, Order)
db.create_all()
Features:
- Thread-safe via
threading.local
connections
- WAL journal mode for concurrent reads
- Foreign key enforcement
- Migration tracking via
_dars_schema_version
table
- Raw SQL:
db.execute()
,
db.fetch_all()
,
db.fetch_one()
# Create
product = Product(name="Widget", price=99, in_stock=True)
product.save()
# Read by ID
product = Product.objects.get(1)
# List all
all_products = Product.objects.all()
# Filter
cheap = Product.objects.filter(price=0)
# Count
count = Product.objects.count()
# Create shorthand
new_product = Product.objects.create(name="Gadget", price=49)
# Update
product.price = 79
product.save()
# Delete
Product.objects.delete(1)
# or
product.delete()
Wire up REST endpoints for all registered models in one call:
from dars.backend.models import register_model_api
register_model_api(app, db, prefix="/api/models")
This generates for each model (e.g.
Product
→ table
products
):
| Method | Endpoint | Description |
|---|---|---|
GET
|
/api/models/products
|
List all |
GET
|
/api/models/products/{id}
|
Get by id |
POST
|
/api/models/products
|
Create |
PUT
|
/api/models/products/{id}
|
Update |
DELETE
|
/api/models/products/{id}
|
Delete |
All endpoints accept and return JSON with automatic Pydantic validation.
Server Actions let you call Python backend functions directly from client-side events with automatic parameter validation, serialization, and error handling.
Use the
@server_action
decorator:
from dars.backend.actions import server_action, call_server
@server_action(csrf_protected=False)
def greet(name: str, count: int = 1) -> list:
"""Returns a list of personalized greetings."""
return [f"Hello {name}! x{i}" for i in range(count)]
@server_action(name="custom_name", auth_required=True)
def admin_action(x: int) -> int:
return x * 2
Parameters are automatically validated via Pydantic models built from type annotations.
Use
call_server()
in event handlers:
Button("Greet", on_click=call_server("greet", name="World", count=3))
# With success/error callbacks:
Button("Save", on_click=call_server(
"save_user",
name="Alice",
on_success=log("Saved!"),
on_error=log("Failed!"),
))
Actions are auto-registered when you start the backend:
app.start_backend() # calls register_actions_on_app(fastapi_app)
Or use autodiscovery for modular projects:
from dars.backend.actions import discover_actions
discover_actions("backend.api")
discover_actions("app.actions")
@server_action(auth_required=True, roles=["admin"])
def delete_user(user_id: int) -> dict:
# Only authenticated users with "admin" role can call this
return {"deleted": user_id}
call_server()
returns a DAP action compatible with the Dars runtime
POST /api/actions/{action_name}
endpoints
Dars provides a complete middleware pipeline for FastAPI/Starlette applications:
| Middleware | Description |
|---|---|
AuthMiddleware
|
JWT Bearer/cookie validation with CSRF protection |
SecurityHeadersMiddleware
|
CSP, HSTS, X-Frame-Options, and more |
CORSMiddleware
|
Configurable cross-origin resource sharing |
RateLimitMiddleware
|
Per-IP / per-user sliding-window rate limiting |
LoggingMiddleware
|
Structured request/response logging |
CompressionMiddleware
|
Gzip response compression |
from dars.backend.middleware import register_default_middlewares
register_default_middlewares(app)
from dars.backend.middleware import DarsMiddleware
from starlette.requests import Request
from starlette.responses import Response
class MyMiddleware(DarsMiddleware):
async def before_request(self, request: Request):
# Return a Response to short-circuit, or None to continue
pass
async def after_response(self, request: Request, response: Response) -> Response:
# Mutate response
return response
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"
Dars currently supports exporting web applications using the standard web exporter. Your exported output includes optimized HTML, CSS, and JavaScript that can be hosted as static assets or served with FastAPI.
For advanced export scenarios, you can extend
dars.exporters.base.Exporter
and implement custom export behavior for your target platform.
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
# Example chaining: use the previous result in the next script
update_op = this().state(text=Arg) # Accesses the entire result
update_op_nested = this().state(text=Arg.content) # Accesses 'result.content'
chained = some_async_action.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. |
and a LOT more..
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.