CSS framework UI kit for
component-framework— Bulma, Bootstrap, Foundation, Fomantic UI, DaisyUI.
Provides ready-to-use Bulma component templates in two first-class template sets:
| Template set | Engine | Frameworks |
|---|---|---|
jinja/ |
Jinja2 / JinjaX | FastAPI, Litestar |
cotton/ |
django-cotton | Django |
Component names are theme-agnostic — <CfCard> and <c-cf.card> render the active theme. Switching CSS frameworks means changing one config line, not hundreds of templates.
# Bulma
pip install "cf-ui[bulma]"
# Tailwind + DaisyUI
pip install "cf-ui[daisy]"
# All themes (Foundation and Fomantic are still stubs)
pip install "cf-ui[all]"All template sets ship in every install. Theme selection is runtime config, not install-time.
# settings.py
INSTALLED_APPS = [
...
"cf_ui.django.CfUiConfig",
]
CF_UI_THEME = "bulma"
TEMPLATES = [{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"APP_DIRS": True,
"OPTIONS": {
"libraries": {"cf_ui": "cf_ui.templatetags.cf_ui"},
},
}]{# base.html #}
{% load cf_ui %}
<!DOCTYPE html>
<html>
<head>
{% cf_ui_head %}
</head>
<body>
{% block content %}{% endblock %}
{% cf_ui_body %}
</body>
</html>
{# any template #}
<c-cf.card>
<c-slot name="header">My Title</c-slot>
Card body content here.
</c-cf.card>
<c-cf.form-field name="email" label="Email" value="{{ form.email.value }}"
error="{{ form.email.errors.0 }}" type="email" />from jinjax import ComponentCatalog
from cf_ui.fastapi import install_cf_ui
catalog = ComponentCatalog()
install_cf_ui(catalog, theme="bulma"){# any JinjaX template #}
<CfCard header="Title">
Card body content.
</CfCard>
<CfFormField name="email" label="Email" value="" extra_class="mb-4" />from litestar.contrib.jinja import JinjaTemplateEngine
from litestar.template import TemplateConfig
from cf_ui.litestar import install_cf_ui
config = TemplateConfig(engine=JinjaTemplateEngine, directory="templates")
install_cf_ui(config, theme="bulma")| Component | Key props |
|---|---|
CfFormField / <c-cf.form-field> |
name, label, value, error, type, required, extra_class |
CfSelect / <c-cf.select> |
name, label, value, error, options |
CfTextarea / <c-cf.textarea> |
name, label, value, error, rows |
CfCheckboxGroup / <c-cf.checkbox-group> |
name, label, choices, selected, error |
| Component | Key props / slots |
|---|---|
CfModal / <c-cf.modal> |
id, extra_class; slots: header, default body, footer |
CfNotification / <c-cf.notification> |
message, type, dismissible |
CfProgress / <c-cf.progress> |
value, max, type, label |
| Component | Key props / slots |
|---|---|
CfCard / <c-cf.card> |
extra_class; slots: header, default body, footer |
CfTable / <c-cf.table> |
columns, rows, hx_target, hx_url |
CfPagination / <c-cf.pagination> |
page, total_pages, hx_target, hx_url |
CfPanel / <c-cf.panel> |
title, open, extra_class; slot: default body |
| Component | Key props / slots |
|---|---|
CfNavbar / <c-cf.navbar> |
extra_class; slots: brand, start, end |
CfBreadcrumb / <c-cf.breadcrumb> |
items (list of {label, url}) |
CfTabs / <c-cf.tabs> |
tabs (list of {id, url}), hx_target |
All components accept an extra_class prop for consumer CSS overrides.
{% cf_ui_head %} and {% cf_ui_body %} inject CDN links for the active theme + AlpineJS. CDN versions are pinned defaults, overridable:
# settings.py
CF_UI_CDN_VERSIONS = {
"bulma": "1.0.2",
"alpinejs": "3.14.1",
}Opt out of CDN entirely — just don't call the tags and load assets yourself.
Jinja2 equivalent:
{% 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") }}CF_UI_THEME picks the CSS framework. Five orthogonal axes decide what the app looks like within it — each keyed on a data attribute, each carrying a closed set of named values:
| Axis | Attribute | Shipped values |
|---|---|---|
| Accent | data-accent |
slate, azure, jade |
| Surface | data-surface |
plain, muted |
| Form | data-form |
sharp, soft, round |
| Density | data-density |
compact, regular, roomy |
| Type | data-type |
system, humanist, mono |
One setting, five attributes:
# settings.py
CF_UI_COMPOSITION = "editorial" # or {"accent": "jade", "density": "compact"}<html lang="en" {% cf_ui_root_attrs %}># FastAPI / Litestar
install_cf_ui(catalog, theme="bulma", composition="editorial")Apps supply their own value sets via CF_UI_AXIS_VALUES (value_sets= for Jinja apps) — the package ships the machinery and a neutral default set, not a brand. data-theme remains the light/dark switch and is not an axis; light and dark are declared independently, never derived by inversion.
Every accent × surface × mode combination in a value set must pass WCAG AA before it ships — the default set is held to that in CI.
→ Full guide: docs/theming.md
The closed value sets are only a promise until something enforces them.
data-accent="hotpink" produces no error on its own — it produces an element
with no accent. cf-ui ships a Tailwind plugin that fails the build on an
unknown axis value, generates the axis CSS and its @media (color-gamut: p3)
layer from the same definition, and can warn with WCAG numbers for every
accent × surface × mode.
@import "tailwindcss";
@plugin "../.venv/lib/python3.12/site-packages/cf_ui/static/cf_ui/cf_ui_tailwind_plugin.mjs";The plugin is vendored in the wheel, not published to npm — a separate npm version could disagree with the installed package about what a valid value is, which is the exact failure it exists to prevent.
→ Full guide: docs/tailwind-plugin.md
{% cf_ui_body %} loads cf_ui_alpine.js (before the Alpine CDN) which registers:
Named components — use x-data="cfModal" to bind from outside:
cfModal—open,show(),toggle(),close(),initModal()cfNavbar—menuOpen,toggle()cfPanel—open,toggle(),initPanel()cfTabs—active,setActive(id),initTabs(),onKeydown(e)
$cf global store — cross-component messaging from any template:
<button @click="$store.cf.notify('Saved!', 'success')">Save</button>
<button @click="Alpine.store('cf').modal.open('confirm-dialog')">Delete</button>Opt out entirely:
{% cf_ui_body alpine=False %}This file is also where the interactive components' accessibility lives — modal
focus trapping and restoration, Escape to close, the tabs roving tabindex. A
theme partial supplies classes and ARIA state; it never implements behavior, so
a new theme inherits all of it.
→ Full guide: docs/accessibility.md
If you need direct access to template directories for custom configuration:
from cf_ui import JINJA_TEMPLATES_DIR, COTTON_TEMPLATES_DIR
# JINJA_TEMPLATES_DIR / "bulma" → Path to Jinja2 templates (theme-prefixed)
# COTTON_TEMPLATES_DIR / "cf" → Path to Cotton component templatesCotton component paths are theme-agnostic. The public components live at
cotton/cf/*.html(notcotton/<theme>/cf/*.html) so cf-ui can sit alongside any consumer project's owntemplates/cotton/<app>/...tree without colliding onCOTTON_DIR. Each one is a thin wrapper that declares its props and includes a theme partial fromcotton/_themes/<theme>/, selected byCF_UI_THEME. Render<c-cf.card>; never reach into_themes/directly.The Jinja side needs no such indirection —
install_cf_ui(catalog, theme=…)registerstemplates/jinja/<theme>/and JinjaX resolves<CfCard>from there.
| Theme | Status |
|---|---|
| Bulma | ✅ v0.1.0 |
| Tailwind + DaisyUI | ✅ — see docs/daisyui.md |
| Bootstrap 5 | ✅ — CSS only, no bootstrap.bundle.js; see docs/bootstrap.md |
| Foundation | 📋 Planned |
| Fomantic UI | 📋 Planned |
Switching is one line — CF_UI_THEME = "daisy" on Django, theme="daisy" on
FastAPI/Litestar — and needs no template edits in the consuming app. An
unimplemented theme name is rejected at startup rather than at first render.
Bootstrap ships CSS only, on purpose. Do not load bootstrap.bundle.js.
Bootstrap's data-bs-* API is a second state owner for the modal, the tabs and
the accordion, and cf_ui_alpine.js is already the first — loading both makes
Alpine.store('cf').modal.open(id) mean something different under this theme
than under every other one. The templates use Bootstrap's classes and markup
structure and wire state through Alpine, so the CDN stylesheet is all a
consuming app needs. docs/bootstrap.md is the decision
record: which of Bootstrap's 12 JS components cf-ui replaces, what to do about
the eight it does not, and what changes at Bootstrap 6.
DaisyUI takes one extra step. It compiles through Tailwind, so Tailwind's content scanner has to reach cf-ui's templates in site-packages or every class in them is tree-shaken away, leaving correct markup with no styling and no error. Get the glob from the package rather than hand-writing it:
python -m cf_ui.themesdocs/daisyui.md covers that, plus disabling Tailwind's preflight while an older framework is still styling the app.
git clone https://github.com/fsecada01/component-framework-ui
cd component-framework-ui
uv pip install -e ".[dev]"
playwright install chromium
just test # unit tests
just test-integration # integration tests
just test-e2e # E2E Playwright tests (requires chromium)
just test-all # everything
just lint # ruff check
just format # ruff format- Python 3.11+
component-framework >= 0.4- Pydantic 2.0+ (only mandatory runtime dependency)
Optional extras:
[django]— Django 4.2+, django-cotton 2.x[fastapi]— FastAPI 0.109+, JinjaX 0.41+[litestar]— Litestar 2.0+, Jinja2 3.1+
MIT