Use label, value, variant and other upstream attributes directly.
Slots
{% slot name %} assigns native named slots.
HTMX
get/post reverse Django routes. target is an element ID; swap chooses replacement behavior.
Themes
Apply ts-theme-light or ts-theme-dark to any container.
Django fields and reloadable regions
{% ts_form form=form post="save" target="editor" swap="outerHTML" %}
{# Fields are rendered automatically. Explicit content replaces them. #}
{% endts_form %}
{% ts_field# field=form.email %}
{% ts_fields# form=form %}
{% ts_hx_content id="status" get="status" %}{% endts_hx_content %}
{# Reload with a body event named update_status. #}
02 / WORKING PATTERN
A complete HTMX workflow.
Search and sort 250 sample clients. Edit a row or create a client. Changes belong to your browser session.
Create in dialogReset demo dataReload status
Client directory
Clear selection
Sample clients
Details
Actions
Reusable AJAX table component and backend
{% ts_ajax_table# id="demo-records" definition=table ajax="demo-data" form_source="record-filters" caption="Sample clients" multi_sort=True selectable=True responsive=True %}
from tcf.tables import Column, Table
class ClientTable(Table):
searching = True
columns = (Column('name', 'Name', order_by=('name',),
search_fields=('name',)),)
def get_queryset(self):
return Client.objects.filter(owner=self.request.user)
# In a CSRF-protected POST endpoint:
return ClientTable(request).response()
# Table prepares and renders only the requested page.
# Default maximum: 100 rows. Sort keys are allowlisted.
Forms in dialogs
Choose Create in dialog or Dialog edit in any row. The same session records, Django validation and table refresh are used. Errors stay in the open dialog; a successful save closes it and shows a toast.
{% ts_button get="demo-dialog-create" target="dialog-editor" swap="outerHTML" data-demo-dialog-form="create" %}Create in dialog{% endts_button %}
{% ts_dialog id="record-dialog" label="Client form" %}
<div id="dialog-editor"></div>
{% endts_dialog %}
# Return demo/dialog_editor.html for GET and invalid POST.
# After a successful POST:
response['HX-Trigger'] = json.dumps({
'update_demo-records': True,
'demo-dialog-saved': {'message': 'Saved client.'}
})
The Dialog & drawer example below includes the matching JavaScript.
POST table-v1 → scoped records → search / order → bounded page → rendered HTML cells → TCFTable → TÜV SÜD adapter
03 / COMPONENT CATALOGUE
Every component. In context.
82 native elements and portable application helpers in 41 live examples. Related children appear with their parent. Expand each example for TCF code and slot names.
No matching components. Try another name.
Actions
Buttons & links
PrimaryDefaultDangerDisabled
Save draftContinueDownload reportSavePrint
ListGrid
Read the quick start
icon adds a left icon; icon_left and icon_right select either or both sides. Native prefix/suffix slots also work. Use variant, size and disabled for actions; href creates a link. Icon-only buttons need a label.
Native cards and buttons inside the portable layout engine. Drag with the Move handle; Connect chooses a target node. Events report changes to your application.
Drag appointments to move them; drag their lower handle to resize. Keyboard: arrows to move, Shift + Up/Down to resize.
Appointment changes are saved in your session.
Switch day/week, navigate, move between resources, and resize appointments. Changes stay in this browser session. Use arrows to move and Shift + Up/Down to resize.
Code & component reference 4 elements
ts_calendar_gridSlots: toolbar
ts_calendar_row
ts_calendar_colSlots: header
ts_calendar_event
{% include 'demo/extras/calendar.html' %}<p id="demo-calendar-events" role="status">Appointment changes are saved in your session.</p>
{% load components tuvsud_theme %}{% ts_calendar_grid# id="demo-calendar" date=demo_calendar_date mode=demo_calendar_mode|default:'day' resources=demo_resources events=demo_events editable=True url="demo-calendar" change_url="demo-calendar" %}
JavaScript
/* Consumer-owned actions; framework helpers expose bubbling events. */
document.addEventListener('click', event => {
const button = event.target.closest('[data-demo-layout]');
if (button) TCFTuvsudDiagram.get('demo-graph').setLayout(button.dataset.demoLayout, 'team');
});
for (const name of ['nodeClick','edgeClick','connect','nodeMoved']) {
document.addEventListener('diagram:' + name, event => {
if (event.detail.diagramId !== 'demo-graph') return;
const {nodeId, edgeId, source, target} = event.detail;
document.getElementById('demo-graph-events').textContent = name + ': ' + (nodeId || edgeId || source + ' → ' + target);
});
}
for (const name of ['open','change','navigate']) {
document.addEventListener('calendar:' + name, event => {
if (event.detail.calendarId !== 'demo-calendar') return;
const {id, start, date, mode} = event.detail;
document.getElementById('demo-calendar-events').textContent = name + ': ' + (id ? id + (start ? ' · ' + start : '') : date + ' · ' + mode);
});
}
document.addEventListener('table:selection', event => {
if (!['demo-local-table','demo-records'].includes(event.target.id)) return;
document.getElementById('demo-selection-events').textContent = 'Selected IDs: ' + (TCFTuvsudTables.selected(event.target.id).join(', ') || 'none');
});
Application helpers
Recursive service tree
CMONE tree dictionaries become native tree items. Selected checkbox leaves submit their short_name; Reset restores their initial selection.
Django minimum-length and numeric rules are reflected in the checklist. Custom policies accept explicit rules; unsupported Django validators remain server-only.
Shift-click a second heading to add its sort priority. Selected IDs survive searches, pages and refreshes.
Choose rows to see their stable record IDs.
Inline records support client search, pagination, Shift-click multi-column sorting, selection by record ID, and mobile details. The live client directory above uses the same enhanced adapter with server data.
Code & component reference 1 element
ts_data_table
{% load components tuvsud_theme %}
{% ts_data_table# id="demo-local-table" columns=demo_local_columns rows=demo_local_rows page_size=5 multi_sort=True selectable=True responsive=True lazy=True caption="Synthetic services" %}
<p class="demo-muted">Shift-click a second heading to add its sort priority. Selected IDs survive searches, pages and refreshes.</p>
<p id="demo-selection-events" role="status">Choose rows to see their stable record IDs.</p>
JavaScript
/* Consumer-owned actions; framework helpers expose bubbling events. */
document.addEventListener('click', event => {
const button = event.target.closest('[data-demo-layout]');
if (button) TCFTuvsudDiagram.get('demo-graph').setLayout(button.dataset.demoLayout, 'team');
});
for (const name of ['nodeClick','edgeClick','connect','nodeMoved']) {
document.addEventListener('diagram:' + name, event => {
if (event.detail.diagramId !== 'demo-graph') return;
const {nodeId, edgeId, source, target} = event.detail;
document.getElementById('demo-graph-events').textContent = name + ': ' + (nodeId || edgeId || source + ' → ' + target);
});
}
for (const name of ['open','change','navigate']) {
document.addEventListener('calendar:' + name, event => {
if (event.detail.calendarId !== 'demo-calendar') return;
const {id, start, date, mode} = event.detail;
document.getElementById('demo-calendar-events').textContent = name + ': ' + (id ? id + (start ? ' · ' + start : '') : date + ' · ' + mode);
});
}
document.addEventListener('table:selection', event => {
if (!['demo-local-table','demo-records'].includes(event.target.id)) return;
document.getElementById('demo-selection-events').textContent = 'Selected IDs: ' + (TCFTuvsudTables.selected(event.target.id).join(', ') || 'none');
});
confirm supplies an HTMX question; Delete already confirms by default. Await TCFTuvsud.confirm() for JavaScript. Cancel, Escape or backdrop leave the action untouched; nested confirmations keep the parent dialog open.
Code & component reference 2 elements
ts_button_create
ts_button_dismiss_modal
{% load components tuvsud_theme %}
<div class="demo-inline">
{% ts_button_create id="helper-open-dialog" icon="add" data-demo-show="helper-action-dialog" %}Create example{% endts_button_create %}
{% ts_button id="demo-confirm-js" icon="help" %}JavaScript confirmation{% endts_button %}
{% ts_button id="demo-confirm-htmx" icon="refresh" get="demo-dialog-content" urlarg="overview" target="demo-confirm-result" confirm="Reload the preview content?" confirm_title="Reload preview" %}HTMX confirmation{% endts_button %}
</div>
<p id="demo-confirm-status" role="status">Choose an action to see Cancel/OK confirmation.</p>
<div id="demo-confirm-result"><p>The HTMX preview loads only after OK.</p></div>
{% ts_dialog id="helper-action-dialog" label="Create action" %}
<p>A native dialog can host a form or HTMX content.</p>
{% ts_button id="demo-confirm-nested" icon="help" %}Confirm inside dialog{% endts_button %}
<p id="demo-confirm-nested-status" role="status">This dialog stays open during confirmation.</p>
{% slot footer %}{% ts_button_dismiss_modal# icon="close" dialog="helper-action-dialog" %}{% endslot %}
{% endts_dialog %}
JavaScript
/* Await the native dialog before performing an action. */
for (const [buttonId, statusId] of [
['demo-confirm-js', 'demo-confirm-status'],
['demo-confirm-nested', 'demo-confirm-nested-status'],
]) {
const button = document.getElementById(buttonId);
button.addEventListener('click', async () => {
const approved = await TCFTuvsud.confirm('Continue with this demo action?', {
title: 'Confirm action', cancel: 'Cancel', ok: 'OK', trigger: button,
});
document.getElementById(statusId).textContent = approved
? 'Confirmed. The demo action ran.' : 'Cancelled. The demo action did not run.';
});
}
Select Create in dialog or Dialog edit to load a Django form.