Creating CKAN Themes
Theme Structure
A CKAN theme should be organized in the following directory structure:
your_theme/
├── templates/
│ ├── (your custom templates)
│ └── macros/
│ └── ui.html
│
├── assets/
│ └── (CSS, JS, and other assets)
│
└── public/
└── (static files served directly)
Create new theme using CLI
New theme can be created by making a copy of an existing theme using CKAN CLI:
ckan theme create THEME_NAME
The folder THEME_NAME will be created in the current directory and will contain all the files with minimalistic theme implementation. It does not look nice, but it does not contain any framework-specific code, so it's the best starting point.
To create a theme by copying an existing theme, specify --base option
ckan theme create THEME_NAME --base ANOTHER_COOOL_THEME
To create theme in a different location, provide path to the base folder after theme name. In this case THEME_NAME will be created inside specified location instead of the current folder.
ckan theme create THEME_NAME /base/location/for/the/new/theme
Registering a Theme
In your extension's plugin.py file, implement the ITheme interface. The key
method is register_themes() which returns a list of Theme objects:
import os
import ckan.plugins as p
from ckanext.theming.interfaces import ITheme
from ckanext.theming.lib import Theme
class YourExtensionPlugin(ITheme, p.SingletonPlugin):
def register_themes(self):
# Return a list of theme Theme objects
root = os.path.dirname(os.path.abspath(__file__))
return [
Theme(
'your_theme',
os.path.join(root, 'themes/your_theme'),
# Optionally specify a parent theme to extend
# parent='parent_theme_name'
),
]
Theme inheritance
Themes can inherit from parent themes to build upon existing functionality:
def register_themes(self):
root = os.path.dirname(os.path.abspath(__file__))
return [
Theme(
'child_theme',
os.path.join(root, 'themes/child_theme'),
parent='parent_theme_name' # Inherits from another theme
),
]
Child themes inherit all macros and templates from the parent, but can selectively override only the components they want to customize. Unimplemented macros fall back to the parent theme.
Additional macro sources
To load macros from an additional file(in addition to macros/ui.html),
implement ITheme.get_additional_theme_ui_sources:
def get_additional_theme_ui_sources(self) -> list[str]:
return ["additional/location/of/macros.html"]
Creating UI Macros
Create themes/your_theme/templates/macros/ui.html with definitions of all the
macros.
{% macro input() %}
...
{% endmacro %}
Macros can be defined elsewhere and re-exported by creating global template variables:
{% import "macros/your_theme_macros/element.html" as element %}
{# Re-export macro #}
{% set button = element.button %}
{% set card = element.card %}
DO NOT define macros inside blocks
Macros defined inside blocks will be unavailable for import. Imports are processed at compile time, while blocks are evaluated at render time, so macros defined inside blocks are invisible for external code.
Flexible themes
When macros created directly inside ui.html or unconditionaly re-exported as
in example above, child theme cannot override these macros. The code below
DOES NOT work:
{# inside child theme's macros/ui.html #}
{% ckan_extends %}
{# Override macro #}
{% set button = my_custom_button %}
Jinja2 processes template hierarchy in a reverse order, so the original
button macro will take precedence over the custom one. To make such overrides
possible, never define macros directly inside ui.html and always use
re-export with the default fallback:
{% import "macros/ui/element.html" as element %}
{# keep definition from the child template or fallback to the original implementation #}
{% set button = button | default(element.button) %}
{% set card = card | default(element.card) %}
{# use the same fallback-strategy for macros defined in the current file. Give
the child template an opportunity to define its own `input` macro and, when
such macro is not defined, use the original `_input` as a fallback implementation #}
{% macro _input() %}
...
{% endmacro %}
{% set input = input | default(_input)%}
Even if you define macros directly inside ui.html, child theme can override
them. But to achieve this, it must implement
ITheme.get_additional_theme_ui_sources. This method returns list of files
with macros that have higher priority than the base ui.html. The downside of
this approach is a bigger list of macro sources which leads to more complex
development and unpredictable macro locations.
Each macro file should contain actual implementations that use appropriate CSS classes for your chosen framework. When implementing macros, follow these conventions:
Parameter Order Consistency
All macros follow the same parameter convention:
contentis the first positional parameter- All other parameters use named parameters with appropriate defaults
- Always use
kwargsfor extra attributes that may be passed to the element
Example
content is the first parameter. Usually content is the most complex part of
the macro output, that must be provided by the caller. Other parameters can
either be specified without a default value if there are no suitable
options(href), or use most expected/neutral values as defaults(type=button,
style=primary or default).
{%- macro button(content, href, type="button", style="primary") -%}
{%- if href -%}
<a {{ ui.util.attrs(kwargs) }} href="{{ href }}" class="btn btn-{{ style }}">
{{ content }}
</a>
{%- else -%}
<button {{ ui.util.attrs(kwargs) }} type="{{ type }}" class="btn btn-{{ style }}">
{{ content }}
</button>
{%- endif %}
{%- endmacro %}
Sometimes macro can work even without content. In the example below, divider without content will be rendered as a plain horizontal line, while divider with content will show this content surrounded by horizontal lines.
{%- macro divider(content) -%}
{%- if content -%}
<div {{ ui.util.attrs(kwargs) }} class="divider-with-content">
<hr><span>{{ content }}</span><hr>
</div>
{%- else -%}
<hr {{ ui.util.attrs(kwargs) }}>
{%- endif %}
{%- endmacro %}
Certain macros do not present any HTML/textual content to user(or do not accept
it at least). In this case it's reasonable to omit content completely. It
would be acceptable to use move alt on the first position and call it
content, but there are two reasons not to do it: it may be misleading;
content usually meant for potentially complex HTML, not only plain
text. src is also is not a good candidate for content, because it's an
attribute, not the actual content shown to user.
{%- macro image(src, alt, height, width) -%}
<img
{{ ui.util.attrs(kwargs) }}
src="{{ src }}"
{%- if alt %} alt="{{ alt }}"{% endif %}
{%- if height %} height="{{ height }}"{% endif %}
{%- if width %} width="{{ width }}"{% endif %}
>
{%- endmacro %}
To render additional attributes on the tag, use ui.util.attrs(kwargs). It
will take attrs parameter from additional arguments provided during macro
call and transform it into attribute string. Certain macro will expose
parameters to simplify attribute assgnment. For example, the following macro
has blank parameter, that modifies attrs parameter. Because of its internal
logic, instead of calling it like ui.link(..., attrs={"target": "_blank",
"rel": "noopener noreferrer"}), you can use shorter form ui.link(..., blank=true)
{%- macro link(content, href, blank) -%}
{%- if blank -%}
{%- do kwargs.setdefault("attrs", {}).setdefault("target", "_blank") -%}
{%- do kwargs.setdefault("attrs", {}).setdefault("rel", "noopener noreferrer") -%}
{%- endif %}
<a {{ ui.util.attrs(kwargs) }} href="{{ href or content }}">{{ content }}</a>
{%- endmacro %}
Note, every macro in the example uses kwargs variables. It must not be added
to signature: whenever Jinja2 sees that kwargs is used inside macro body, it
implicitely adds **kwargs to macro signature; attempt to do it explicitely
will cause en error.
Because of kwargs usage, users can call any of these macros with additional
parameters, even if current theme does not process them. In this way, when user
switches from a different theme, that has more arguments inside macro
definition, pages will not break because of invalid call payload.
UI Utilities
The theming system provides utility functions accessible via ui.util:
ui.util.attrs(kwargs)
Helper to render HTML attributes from a dictionary. It extracts from kwargs
parameters with names attrs, aria, data, on, hx and builds attribute
string from them. Also it has second argument, that can be used to specify
default attributes(attrs), that user can override during macro call:
{% macro button(content) %}
<button {{ ui.util.attrs(kwargs, {"class": "btn"}) }}>
{{ content }}
</button>
{% endmacro %}
ui.util.call(element, *args, **kwargs)
Call an inline element as a block element. It takes the content of the call block and pass it as a first argument of the called macro. Use this to add complex content with nested HTML into element.
{% call ui.util.call(ui.button, type="submit") %}
<i class="fa fa-info-circle"></i>
Click
{% call%}
{# approximately the same but less readable version #}
{{ ui.call('<i class="fa fa-info-circle"></i> Click'|safe, type="submit") }}
ui.util.map(element, items, *args, **kwargs)
Map an element over a collection
{{ ui.util.map(ui.button, ["Click", "Press", "Push"], type="submit") }}
ui.util.now()
Get the current UTC datetime
ui.util.id(value, prefix="id-")
Generate a unique identifier(if value is empty) or transform value into stable UUID.
ui.util.tag(content, tag, **kwargs)
Renders an arbitrary tag. Use it to produce dynamic wrappers depending on condition. If tag name is empty, conent will be printed as-is, without a wrapper.
{{ ui.util.tag(
"Hello world",
"span" if inline_tag else "div",
attrs={"class": "wrapper"}) }}
ui.util.keep_item(category, key, value) and ui.util.pop_items(category, key=None)
keep_item store items in UI storage. Similar to h.flash_success, but for
arbitrary data.
pop_items retrieve and remove items from UI storage. Similar to
h.get_flashed_messages, but for arbitrary data.
Example
{%- macro button_group(items) -%}
<div {{ ui.util.attrs(kwargs) }} class="btn-group">
{{ ui.util.map(ui.button, items, on={"click": "alert(42)"}) }}
</div>
{%- endmacro %}
{# Using call with util.call #}
{% call ui.util.call(ui.button, style="primary", attrs={"id": ui.util.id()}) %}
{{ ui.icon("home") }}
Click me!
{% endcall %}
Accessibility Considerations
When implementing theme components, ensure proper accessibility support by using appropriate ARIA attributes and semantic HTML:
{%- macro button(content, href, type="button", style="primary") -%}
{%- if href -%}
<a {{ ui.util.attrs(kwargs, {"aria-label": content}) }}
href="{{ href }}"
class="btn btn-{{ style }}">
{{ content }}
</a>
{%- else -%}
<button {{ ui.util.attrs(kwargs, {"aria-label": content}) }}
type="{{ type }}"
class="btn btn-{{ style }}">
{{ content }}
</button>
{%- endif %}
{%- endmacro %}
{%- macro input(content, name, id, label, value, required, placeholder, type="text", errors=[]) -%}
{%- set field_id = id or (name and ("field-" ~ name)) or (label and ui.util.id()) or "" -%}
{%- set error_id = ui.util.id() if errors -%}
{%- set help_id = ui.util.id() if content -%}
<div>
{%- if label -%}
<label for="{{ field_id }}">{{ label }}</label>
{%- endif %}
{%- if content -%}
<div class="input-help" id="{{ help_id }}">{{ content }}</div>
{%- endif %}
<input
{{ ui.util.attrs(kwargs) }}
type="{{ type }}"
{%- if name %} name="{{ name }}"{% endif %}
id="{{ field_id }}"
{%- if value %} value="{{ value }}"{% endif %}
{%- if placeholder %} placeholder="{{ placeholder }}"{% endif %}
{%- if required %} required{% endif %}
{%- if content %} aria-describedby="{{ help_id }}"{% endif %}
{%- if errors %} aria-invalid="true"{% endif %}
>
{%- if errors %}
<span id="{{ error_id }}">{{ ui.field_errors(errors) }}</span>
{%- endif %}
</div>
{%- endmacro %}
Use proper ARIA attributes (aria-label, aria-describedby, aria-invalid, aria-hidden, etc.), semantic HTML elements, and ensure keyboard navigation support.
Using UI Macros in Templates
Once a theme is activated by setting its name as a value for ckan.ui.theme
config option, UI macros can be used in templates:
{{ ui.button("Click Me", style="primary", type="button") }}
{{ ui.card("Card content here", title="My Card") }}
{{ ui.alert("Success message", style="success") }}
{{ ui.link("Visit CKAN", href="https://ckan.org", blank=true) }}
All parameters except for content must be passed to macro by name. This
simplifies transition between themes, when macros expect different set of
arguments or define them in different order. content always comes first when
it's present, that's why it's safe to pass it without name, but all other
arguments have no recommended order and every theme is free to choose according
to its preferences.
CLI Tools for Theme Development
The theming system provides comprehensive CLI tools for theme development and management:
Theme Management
# List available themes
ckan theme list
# Create a new theme with all required structure
ckan theme create mytheme
# Create a new theme in a specific location
ckan theme create mytheme /path/to/themes
Component Management
# List available components for the current theme
ckan theme component list
# Analyze UI components and their implementations.
# Specify component names to reduce output
ckan theme component analyze
ckan theme component analyze link button card
# Check if a theme implements all known UI components
ckan theme component check
Template Management
# List template files
ckan theme template list
# Verify that a theme contains all required templates
ckan theme template check
# Analyze theme templates and their structure
ckan theme template analyze
ckan theme template analyze header.html package/search.html
ckan theme template analyze --relative-filename
Endpoint Analysis
# List registered Flask endpoints
ckan theme endpoint list
# List variants of Flask endpoints
ckan theme endpoint variants
ckan theme endpoint variants dataset.search dataset.read
# Observe the template and context variables used by a Flask endpoint
ckan theme endpoint observe dataset.search
ckan theme endpoint observe dataset.read id=my-dataset -v
ckan theme endpoint observe dataset.read --auth-user admin id=my-dataset
Configuration
To use a theme, configure it in your CKAN configuration:
ckan.plugins = ... theming your_theme_plugin
ckan.ui.theme = your_theme
Reference Implementation
The bare theme in this extension serves as a reference implementation showing
the minimal structure needed for a theme. It can be used as a starting point
for building new themes by running:
ckan theme create mytheme
This creates a new theme based on the bare theme structure with all required components.