Citry is a templating engine that brings the best of Vue, React, Django, and Jinja to every language.
Use the same Vue-like API to write templates everywhere - Python, JS/TS, PHP, Go, or Rust:
<c-Card title="Welcome" c-class="card_classes">
<c-fill name="body">
<c-for each="item in items">
<c-Item c-data="item" />
</c-for>
</c-fill>
<c-fill name="footer">
<button c-disabled="is_loading">Submit</button>
</c-fill>
</c-Card>Use Citry to build UI, HTML, XML, SVG, or anything that serializes to text.
Citry is:
- Familiar - If you know HTML and Vue/React, you're ready
- Simple - Just 2 rules and 12 special tags
- Fast - Rust-powered parsing
- Safe - Expressions are sandboxed to block dangerous operations
- Universal - One template language for your entire stack
Citry extends HTML with just two rules:
<c-*>tags are components - Any tag starting withc-is a component (or special tag)c-*attributes are dynamic - Any attribute starting withc-evaluates as an expression
That's it. If you know HTML, you already know 90% of Citry.
<!-- Static HTML attribute -->
<div class="container">
<!-- Dynamic attribute (expression) -->
<div c-class="dynamic_classes">
<!-- Your component -->
<c-MyComponent title="Hello"></c-MyComponent>
</div>
</div>Beyond custom components, Citry provides 12 built-in tags for common patterns.
With just these 12 tags, Citry is as versatile as Vue or React:
| Tag | Purpose | Example |
|---|---|---|
| Control flow: | ||
<c-if> |
Conditional rendering | <c-if cond="is_visible">...</c-if> |
<c-elif> |
Else-if branch | <c-elif cond="is_other">...</c-elif> |
<c-else> |
Else branch | <c-else>...</c-else> |
<c-for> |
Loop iteration | <c-for each="item in items">...</c-for> |
<c-empty> |
Empty state for loops | <c-empty>No items</c-empty> |
| Components: | ||
<c-slot> |
Define insertion point | <c-slot name="header" /> |
<c-fill> |
Fill a slot | <c-fill name="header">...</c-fill> |
<c-component> |
Dynamic component | <c-component c-is="comp_name" /> |
| Misc: | ||
<c-provide> |
Dependency injection (ContextProvider) | <c-provide key="theme" mode="dark">...</c-provide> |
<c-css> |
Render components' CSS here | <c-css /> |
<c-js> |
Render components' JS here | <c-js /> |
<c-raw> |
Unprocessed content | <c-raw>{{ not evaluated }}</c-raw> |
Control flow can also be written as attributes on regular elements:
If / elif / else:
<!-- As tags -->
<c-if cond="is_visible">
<div class="panel">Content</div>
</c-if>
<c-elif cond="is_admin">
<div class="panel">Other content</div>
</c-if>
<c-else>
<c-Login />
</c-if>
<!-- As attributes (shorter!) -->
<div c-if="is_visible" class="panel">Content</div>
<div c-elif="is_admin" class="panel">Other content</div>
<c-Login c-else />For / empty:
<!-- Loops work the same way -->
<ul>
<li c-for="item in items">{{ item.name }}</li>
<li c-empty>No items found</li>
</ul>Use {{ }} to embed expressions anywhere in the text.
The expression inside {{ }} is written in your host language:
<!-- Python -->
<p>{{ len(items) }} items</p>
<p>total: {{ sum(item.price for item in items) }}</p>
<!-- JavaScript -->
<p>{{ items.length }} items</p>
<p>total: {{ items.reduce((a, b) => a + b.price, 0) }}</p>
<!-- PHP -->
<p>{{ count($items) }} items</p>
<p>total: {{ array_sum(array_column($items, 'price')) }}</p>
<!-- Result: -->
<p>3 items</p>
<p>total: 60</p>To dynamically compute a tag attribute, prefix it with c-.
The attribute value is then treated as an expression (same as inside {{ }}).
The c- prefix is stripped from the rendered attribute.
Just like {{ }}, the expression inside is written in your host language:
<!-- Input (button_type = "primary", is_loading = True) -->
<div c-class="'btn ' + button_type">
<button c-disabled="is_loading">Submit</button>
</div>
<!-- Result: -->
<div class="btn primary">
<button disabled>Submit</button>
</div>Dynamic c-* attributes can contain nested templates instead of expressions.
Simply write the HTML inside the quotes:
<c-Card
title="My Card"
c-footer="
<footer>
<p>Made with ❤️</p>
</footer>
"
/>The nested template must have a single root tag. To pass plain text or multiple root elements,
wrap them in <> and </> (React-style fragments):
<c-Card
c-body="<>
<p>First paragraph</p>
<p>Second paragraph</p>
</>"
c-footer="<>Just some text</>"
/>Use c-bind to spread a dictionary of attributes onto an element:
<!-- Input (item.id = 123) -->
<div c-bind="{ 'class': 'btn', 'disabled': True, 'data-id': item.id }"></div>
<!-- Result: -->
<div class="btn" disabled data-id="123"></div>You can use c-bind multiple times and interlace it with regular or dynamic attributes. Attributes are applied left to right - in case of duplicates, the last one wins:
<!-- Input -->
<div
class="default"
c-bind="{ 'class': 'from-bind', 'id': 'first' }"
c-class="'override'"
c-bind="{ 'id': 'second' }"
></div>
<!-- Result (last value for each attribute wins): -->
<div class="override" id="second"></div>Citry supports a Vue-like slot system. This consists of 2 parts:
-
Inside the component template, define insertion points with
<c-slot>:<!-- Modal.html --> <div class="modal"> <header>{{ title }}</header> <main> <c-slot /> </main> <footer> <c-slot name="actions" /> </footer> </div>
-
When using the component inside another template, pass content to slots with
<c-fill>:<!-- Using a component with named slots --> <c-Modal title="Confirm"> <c-fill name="default"> <p>Are you sure?</p> </c-fill> <c-fill name="actions"> <button>Cancel</button> <button>Confirm</button> </c-fill> </c-Modal>
If you omit the name attribute on <c-slot>, it defaults to "default".
Slots can also be marked as required, causing an error if no <c-fill> is provided.
<c-slot name="actions" required />When you pass a <c-fill> to a component, the <c-slot> renders
the provided fill in its place.
When there is no <c-fill> for the corresponding <c-slot>, it will render
the body within the <c-slot> ... </c-slot> tags as a fallback.
<!-- Button.html -->
<button>
<c-slot>Click me</c-slot>
</button>
<!-- Usage without fill (renders fallback): -->
<c-Button />
<!-- Result: <button>Click me</button> -->
<!-- Usage with fill (renders fill): -->
<c-Button>Submit</c-Button>
<!-- Result: <button>Submit</button> -->When passing only content to the default slot, you can omit <c-fill>:
<!-- These are equivalent: -->
<c-Modal title="Confirm">
<c-fill name="default">
<p>Are you sure?</p>
</c-fill>
</c-Modal>
<c-Modal title="Confirm">
<p>Are you sure?</p>
</c-Modal>Slots can expose data to the fill, similar to Vue's scoped slots.
1. Passing data from <c-slot> - Any extra attributes on <c-slot> become slot data:
<!-- UserList.html -->
<c-for each="user in users">
<c-slot name="item" c-user="user" c-index="loop.index" />
</c-for>2. Accessing data in <c-fill> - Use the data attribute to bind slot data to a variable:
<c-UserList c-users="users">
<c-fill name="item" data="slot">
<div>{{ slot.index }}: {{ slot.user.name }}</div>
</c-fill>
</c-UserList>3. Accessing slot fallback - Use the fallback attribute to access the slot's fallback content:
<!-- Component template with fallback slot content -->
<c-slot name="title">
<h1>Fallback Title</h1>
</c-slot>
<!-- Usage: wrap the fallback with extra markup -->
<c-MyComponent>
<c-fill name="title" fallback="fallback">
<div class="custom-wrapper">{{ fallback }}</div>
</c-fill>
</c-MyComponent>Citry supports three types of comments:
1. HTML comments - Preserved in the rendered output:
<!-- This comment will appear in the final HTML -->
<div>Content</div>2. Template comments - Stripped from output entirely:
{# This comment won't appear in the rendered HTML #}
<div>Content</div>3. Expression comments - Language-specific comments inside {{ }} or c-* attributes:
<!-- Python example -->
<div c-class="get_classes() # Fetch dynamic classes">
{{ user.name # Display username }}
</div>The expressions inside Citry templates (inside {{ }} and c-*) are language-specific.
E.g. when you download the citry Python package, the expressions will be Python code:
<div>
{{ len(user.items) if user.logged_in else 0 }}
</div>Citry is designed to integrate with any programming language within its expressions.
This means that the same Citry package can be released for any programming languages.
Progress on supported languages:
| Language | Status | Integration |
|---|---|---|
| Python | ✅ | PyO3/maturin |
| JS/TS | ❌ | wasm-bindgen |
| PHP | ❌ | FFI |
| Go | ❌ | cgo |
| Rust | ❌ | Native |
Help us implement Citry package for your language!
Star this repo to follow development.
pip install citryFor development setup and codebase details, see docs/codebase.md.
MIT License - see LICENSE for details.
This project is the continuation of work originally done in django-components and django-components/djc-core.