Quickstart¶
Pick your web framework. Each path gets you to a page rendering a cf-ui component with the theme's stylesheet attached.
Install first — see Installation.
Django¶
1. Settings¶
# settings.py
INSTALLED_APPS = [
# ...
"django_cotton",
"cf_ui.django.CfUiConfig",
]
CF_UI_THEME = "bulma"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"libraries": {"cf_ui": "cf_ui.templatetags.cf_ui"},
},
}
]
Two details that bite:
Register the AppConfig by its full class path
"cf_ui.django.CfUiConfig", not "cf_ui.django". Django 4.2 removed
default_app_config, so the bare module path won't find the config class.
The libraries entry is not optional
cf-ui's app is named cf_ui.django, which stops Django's templatetag
autodiscovery from finding cf_ui/templatetags/. Without the explicit
libraries mapping, {% load cf_ui %} raises
TemplateSyntaxError: 'cf_ui' is not a registered tag library.
CfUiConfig validates CF_UI_THEME (and CF_UI_COMPOSITION) at startup — a
typo fails the boot, not the first request.
2. Base template¶
{# templates/base.html #}
{% load cf_ui %}
<!DOCTYPE html>
<html lang="en" {% cf_ui_root_attrs %}>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% cf_ui_head %}
</head>
<body>
{% block content %}{% endblock %}
{% cf_ui_body %}
</body>
</html>
{% cf_ui_head %} emits the theme stylesheet; {% cf_ui_body %} emits
cf_ui_alpine.js followed by the Alpine CDN, in that order — cf-ui's file
registers the components Alpine then initializes, so the order is load-bearing.
3. Use a component¶
{# templates/home.html #}
{% extends "base.html" %}
{% block content %}
<c-cf.card header="Welcome">
Card body content here.
</c-cf.card>
<c-cf.notification message="Saved." type="success" />
<c-cf.form-field name="email" label="Email" type="email"
value="{{ form.email.value|default:'' }}"
error="{{ form.email.errors.0|default:'' }}" />
{% endblock %}
Component names are <c-cf.NAME> where NAME is the kebab-case component
name — see Components for all fourteen.
FastAPI¶
install_cf_ui registers cf-ui's templates on a JinjaX catalog under the Cf
prefix.
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from jinjax import Catalog
from cf_ui.fastapi import install_cf_ui
app = FastAPI()
catalog = Catalog()
install_cf_ui(catalog, theme="bulma")
@app.get("/", response_class=HTMLResponse)
async def home() -> str:
return catalog.render("Cf:Card", header="Welcome", _content="Card body.")
The class is Catalog
JinjaX exports Catalog. There is no ComponentCatalog — importing that
name raises ImportError.
_content, not content
content is reserved by JinjaX: as a keyword it becomes the slot wrapper,
not a string. Pass slot content from Python as _content=. In a template
the component's body fills the same slot, so <Cf:Card>Body</Cf:Card>
needs no special casing.
From your own templates¶
Add your template folder to the same catalog and render components with the
Cf: prefix:
{#def title="" #}
<Cf:Card :header="title">
Card body content.
</Cf:Card>
<Cf:FormField name="email" label="Email" value="" extra_class="mb-4" />
Use :prop= to pass a value, not prop="{{ }}"
JinjaX does not interpolate {{ … }} inside an attribute — the braces
arrive at the component as literal text, with no error. A plain
prop="text" is a literal string; anything computed needs the colon
binding: :header="title", :rows="6", :options="plan_options".
The prefix separator is a colon
<Cf:Card>. Not <CfCard> and not <Cf.Card> — both raise
ComponentNotFound. The same name works in
catalog.render("Cf:Card", ...).
Assets¶
Mount cf-ui's static directory and pull the asset macros into your layout:
from fastapi.staticfiles import StaticFiles
import cf_ui
app.mount(
"/static/cf_ui",
StaticFiles(directory=cf_ui.__path__[0] + "/static/cf_ui"),
name="cf_ui_static",
)
{% from "cf_ui/assets.jinja" import cf_ui_head, cf_ui_body %}
{{ cf_ui_head(theme="bulma") }}
{{ cf_ui_body(theme="bulma", cf_alpine_url="/static/cf_ui/cf_ui_alpine.js") }}
Litestar¶
install_cf_ui appends cf-ui's template directory to the config and chains an
engine callback that registers the theme-axis globals.
from litestar import Litestar, get
from litestar.plugins.jinja import JinjaTemplateEngine
from litestar.response import Template
from litestar.template.config import TemplateConfig
from cf_ui.litestar import install_cf_ui
config = TemplateConfig(engine=JinjaTemplateEngine, directory="templates")
install_cf_ui(config, theme="bulma")
@get("/")
async def home() -> Template:
return Template(template_name="home.html", context={"title": "Welcome"})
app = Litestar(route_handlers=[home], template_config=config)
JinjaTemplateEngine moved in Litestar 2.22
It now lives at litestar.plugins.jinja. The old
litestar.contrib.jinja path still works but warns, and goes away in
Litestar 3.0.
Component tags work here exactly as they do on FastAPI. install_cf_ui
installs a JinjaX catalog onto Litestar's own Jinja2 environment, so ordinary
Litestar templates can use <Cf:…> directly:
{# templates/home.html #}
{% from "cf_ui/assets.jinja" import cf_ui_head, cf_ui_body %}
<!DOCTYPE html>
<html lang="en">
<head>{{ cf_ui_head(theme="bulma") }}</head>
<body>
<Cf:Card :header="title">
Card body content.
</Cf:Card>
<Cf:Notification message="Saved." type="success" />
{{ cf_ui_body(theme="bulma") }}
</body>
</html>
Litestar gained component rendering in 0.2.0
Before that, <Cf:Card> reached the browser as literal text and
{% from "cf_ui/assets.jinja" %} raised TemplateNotFound — the
installer registered neither a catalog nor the directory the macros live
in. Both are fixed, and a live Litestar app now renders components in the
integration suite.
Next¶
- Getting started — themes, axes, assets, and escaping in order.
- Components — every component and prop.
- Use cases — worked examples with HTMX and Alpine.