Components

Caspian components are Python functions decorated with @component. Author them in Python, then import and render them from HTML with x-* tags. Start with direct string output, then move to same-name HTML templates when the UI needs more markup or PulsePoint behavior.

Keep reusable UI in src/components/, keep route-owned markup in src/app/, and keep non-UI helpers in src/lib/. The authored HTML contract stays the same everywhere: one root, plain <script> inside that root, and native PulsePoint event attributes.

1. The Basics

A standard component handles prop merging and returns an HTML string. Use merge_classes to ensure Tailwind classes don't conflict.

Developer Speed Tip

Don't write boilerplate. If you have the VS Code Extension installed, just type:

caspcom + Tab

It automatically generates the standard structure seen below.

src/components/Container.py
from casp.html_attrs import get_attributes, merge_classes
from casp.component_decorator import component

@component
def Container(**props):
    # 1. Extract 'class' to merge it safely
    incoming_class = props.pop("class", "")
    final_class = merge_classes("mx-auto max-w-7xl px-4", incoming_class)

    # 2. Extract children (inner HTML)
    children = props.pop("children", "")

    # 3. Generate remaining attributes (id, data-*, etc.)
    attributes = get_attributes(
        "class": final_class
    , props)

    return f'<div attributes>children</div>'

2. Type-Safe Props

Want TypeScript-like validation? Use Python's Literal type. Your editor will autocomplete variants, and linter will catch typos.

src/components/ui/Button.py
from typing import Literal, Any
from casp.component_decorator import component

# Define strict types for autocomplete
ButtonVariant = Literal["default", "destructive", "outline"]
ButtonSize = Literal["default", "sm", "lg"]

@component
def Button(
    children: Any = "",
    # Editor will now suggest: "default" | "destructive" | "outline"
    variant: ButtonVariant = "default", 
    size: ButtonSize = "default",
    **props,
) -> str:
    # ... implementation (merging classes based on variant/size) ...
    return f"<button attrs>children</button>"

3. Same-Name HTML Templates

When a component grows beyond a short return string, move the markup into a same-name .html file and use render_html(...) from the Python companion. The HTML template can mix server-side Jinja with PulsePoint state and events while staying HTML-first.

Counter.py
from casp.component_decorator import component, render_html

@component
def Counter(label: str = "Clicks") -> str:
  return render_html(__file__, 
    "label": label,
  )
Counter.html
<div>
  <h3>{{ label }}</h3>
  <button onclick="setCount(count + 1)">
     count
  </button>

  <script>
    const [count, setCount] = pp.state(0);
  </script>
</div>

Passing Context Dictionaries

When you need precise control over variable names, or when passing state handlers that don't map 1:1 to props, you can pass a dictionary explicitly as the second argument to render_html.

src/components/ui/Dialog.py
from casp.component_decorator import component, render_html

@component
def Dialog(open: str = "", setOpen: str = ""):
    
    # You can now pass a dictionary context directly.
    # Useful for remapping props or injecting derived state.
    return render_html(__file__, 
        "open": open,
        "setOpen": setOpen,
        "dialogId": "main-dialog"
    )

4. Async Components

Components now support async and await when your UI needs to wait for server-side work before rendering. This follows a familiar FastAPI-style pattern: keep simple components synchronous, and switch to async only when the component needs to await I/O such as a database query, an external API call, file loading, or other asynchronous operations.

Mental Model

Think of PulsePoint components like FastAPI route functions: use a normal function for fast static work, and use an async function when you need to await something. This keeps your code clean, explicit, and easy to scale.

When to Use Async Components

Use an async component when the render phase depends on data that is not immediately available. A few common examples:

  • Awaiting a database query before building the final HTML.
  • Loading related records such as users, posts, notifications, or products.
  • Fetching data from an internal API or service layer.
  • Reading files or remote resources needed at render time.

If the component only formats props, merges classes, or returns static markup, keep it synchronous. Async is powerful, but it should be used only where waiting is actually required.

ProfileCard.py
from casp.component_decorator import component, render_html

@component
async def ProfileCard(user_id: str) -> str:
    # Wait for data before rendering the UI
    user = await get_user_by_id(user_id)

    # Return a normal component template once the data is ready
    return render_html(__file__, 
        "user": user
    )
ProfileCard.html
<div class="rounded-xl border border-border p-4 bg-card text-card-foreground">
  <h3 class="text-lg font-semibold">user.name</h3>
  <p class="text-sm text-muted-foreground">user.email</p>
</div>

FastAPI-Style Example

The pattern is intentionally familiar. In FastAPI, you write async def when a route needs to await I/O. PulsePoint components now work the same way. The component itself becomes async, awaits the required operation, and then returns the final rendered markup.

src/components/DashboardStats.py
from casp.component_decorator import component, render_html

@component
async def DashboardStats() -> str:
    # Example: await database or service calls before rendering
    total_users = await get_total_users()
    total_posts = await get_total_posts()
    total_sales = await get_total_sales()

    return render_html(__file__, 
        "total_users": total_users,
        "total_posts": total_posts,
        "total_sales": total_sales,
    )

Best Practice

Keep data fetching inside the async component only when that data is truly part of the component's render contract. If the data belongs to a page-level workflow, load it at the page level and pass it down as props. This keeps components reusable and avoids unnecessary coupling.

Sync vs Async

Use Sync Components

For static layout, prop formatting, class merging, simple template composition, and any component that does not need to wait for I/O.

Use Async Components

For database reads, service calls, file access, remote fetches, or any render path that depends on awaited server-side data.

5. Server-Side Logic (RPC)

Components can also host server-side logic using the @rpc decorator. This allows you to encapsulate business logic directly within the component file.

Important Note

RPCs in components are Global. Unlike Pages (which are scoped to the URL), component RPCs are registered by their function name. Ensure unique names to avoid conflicts.

src/components/ForgotPassword.py
from casp.rpc import rpc
from casp.component_decorator import component, render_html

@rpc()
def send_forgot_password_email(email: str) -> dict:
    # Server-side logic (e.g., SMTP, Database)
    print(f"Sending password reset to email")
    return "status": "success", "message": "Email sent."

@component
def ForgotPassword(
    openDialog: str = "", 
    setOpenDialog: str = ""
) -> str:
    return render_html(__file__, 
        openDialog=openDialog, 
        setOpenDialog=setOpenDialog
    )

6. Component Composition

Compose reusable components by importing the Python export with a top-of-file @import comment and rendering the kebab-cased x-* tag in authored HTML.

Crucial Rule: Every component HTML template must return a single parent element. Top-of-file @import directives may appear above that root, but the template itself still needs one final root and any owned script must stay inside it.

users/index.html (Parent View)
<!-- Import and use your custom component, passing state as props -->
<!-- @import CreateUpdateDialog from "../../components/CreateUpdateDialog.py" -->

<section>
  <x-create-update-dialog
    open-dialog="openCreateUpdateDialog"
    set-open-dialog="setOpenCreateUpdateDialog"
    selected-user="selectedUser"
    users="users"
    set-users="setUsers"
  />
</section>
components/CreateUpdateDialog.py
from casp.component_decorator import component, render_html

# Define the component and accept the props passed from the parent
@component
def CreateUpdateDialog(
    openDialog: str, 
    setOpenDialog: str, 
    selectedUser: str | None = None, 
    users: str | None = None, 
    setUsers: str | None = None
):
    return render_html(__file__, 
        "openDialog": openDialog,
        "setOpenDialog": setOpenDialog,
        "selectedUser": selectedUser,
        "users": users,
        "setUsers": setUsers
    )
components/CreateUpdateDialog.html
<div>
  <x-dialog open="openDialog" on-open-change="setOpenDialog">
    <!-- ... dialog content ... -->
  </x-dialog>

  <script>
    const  openDialog, setOpenDialog, selectedUser  = pp.props;

    pp.effect(() => 
      if (selectedUser) 
        setUserName(selectedUser.name);
      
    , [selectedUser]);
  </script>
</div>