Theming Guide
Learn how to create custom themes for your Accent CMS site.
Styling for Markdown-Based Content
Since Accent CMS renders markdown to HTML, the best CSS frameworks are those that style semantic HTML elements directly. Classless CSS frameworks work exceptionally well because they require no special markup - your markdown automatically looks great.
Key considerations:
- Semantic HTML elements (
<article>,<header>,<nav>,<main>,<footer>) are styled automatically - Typography is handled out of the box - headings, paragraphs, lists, blockquotes
- Code blocks with syntax highlighting work with most frameworks
- Tables and forms render properly without custom classes
- Dark mode support via
prefers-color-schemeor data attributes
Avoid frameworks that require heavy class annotations (like Tailwind or Bootstrap utilities) since markdown output cannot include arbitrary classes.
Available Themes
accent init installs the simple starter theme (and the default theme powers the documentation site). Starter templates with ready-made themes are available via accent init --list.
| Theme | Framework | Best For |
|---|---|---|
| simple | Simple.css | Clean, classless styling with dark mode (installed by accent init) |
| default | Custom CSS | Starting point, minimal styling (powers the docs site) |
Beyond the bundled themes, published themes install straight from the ecosystem registry – signature- and checksum-verified:
accent theme search dark # find a theme by name, description, or tag accent theme list --remote # or browse everything the registry publishes accent theme install agentic # install one accent theme outdated # see which installed themes are behind accent theme update agentic # move one to the latest version
Installing does not activate: set theme.name in config.yaml to switch to
it.
A theme is yours to edit once installed – which is exactly why
accent theme update will not silently overwrite your work. If any installed
file was modified, added, or removed since install, the update refuses and
lists each difference; re-run with --force to discard the listed changes,
or copy them somewhere safe first. See The Hub for the full
theme, plugin, and template distribution story, including how retracted
versions are handled.
Classless Frameworks (Recommended)
Simple and Pico themes use classless CSS frameworks that style HTML elements directly. Your markdown content renders beautifully without any template modifications:
- Automatic dark/light mode based on system preference
- Responsive typography and spacing
- Styled tables, code blocks, and forms
- Minimal CSS footprint
Traditional Frameworks
The Cinder theme demonstrates using a class-based framework (Bootstrap). This approach requires more template customization but offers more control over component styling.
Lightweight Grid Frameworks
The Skeleton theme uses a minimal CSS boilerplate (~400 lines) that provides:
- 12-column responsive grid system
- Base typography for markdown elements
- Styled buttons, forms, tables, and code blocks
- Raleway font for clean typography
How Skeleton maps to markdown:
| Markdown | HTML Output | Skeleton Styling |
|---|---|---|
# Heading | <h1> | 4-5rem, weight 300, letter-spacing |
**bold** | <strong> | Inherited |
[link](url) | <a> | Cyan color (#1EAEDB) |
`code` | <code> | Gray background, rounded corners |
```code | <pre><code> | Block display, padding |
- list | <ul><li> | Circle markers, proper spacing |
> quote | <blockquote> | 2.5rem margin |
| ` | table | ` |
Using the grid in templates:
Skeleton’s grid is available for custom layouts in templates:
<div class="row">
<div class="eight columns">
{{ page.content | safe }}
</div>
<div class="four columns">
<aside>Sidebar content</aside>
</div>
</div>
Grid classes: .one.column through .twelve.columns, plus .one-half, .one-third, .two-thirds.
Theme Structure
A theme consists of templates, assets, and a configuration file:
site/themes/my-theme/
├── theme.yaml # Theme configuration
├── templates/ # Jinja2 templates
│ ├── base.html.jinja
│ ├── default.html.jinja
│ └── blog.html.jinja
└── assets/ # Static files
├── css/
│ └── style.css
├── js/
│ └── main.js
└── images/
Theme Configuration
The theme.yaml file defines your theme:
name: My Theme description: A custom theme for my site author: Your Name version: 1.0.0 assets: css: - css/style.css js: - js/main.js
Templates
Accent CMS uses MiniJinja, a Jinja2-compatible template engine.
Base Template
Create templates/base.html.jinja:
<!DOCTYPE html>
<html lang="{{ site.language }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ page.title }} - {{ site.name }}{% endblock %}</title>
{% for css in theme.assets.css %}
<link rel="stylesheet" href="/theme/assets/{{ css }}">
{% endfor %}
</head>
<body>
<header>
<nav>
<a href="/" class="site-title">{{ site.name }}</a>
<ul>
{% for p in pages %}
{% if p.menu.visible %}
<li><a href="{{ p.url }}">{{ p.title }}</a></li>
{% endif %}
{% endfor %}
</ul>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© {{ site.name }}</p>
</footer>
{% for js in theme.assets.js %}
<script src="/theme/assets/{{ js }}"></script>
{% endfor %}
</body>
</html>
Page Template
Create templates/default.html.jinja:
{% extends "base.html.jinja" %}
{% block content %}
<article>
<h1>{{ page.title }}</h1>
{% if page.date %}
<time>{{ page.date }}</time>
{% endif %}
<div class="content">
{{ page.content | safe }}
</div>
{% if page.tags %}
<div class="tags">
{% for tag in page.tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
</article>
{% endblock %}
Available Template Variables
Site Variables
| Variable | Description |
|---|---|
site.name | Site name from config |
site.description | Site description |
site.language | Site language code |
site.main_site | Optional main-site link block from config; none when unset |
site.main_site.url | Main-site URL the brand wordmark links to (non-empty; enforced at config load) |
site.main_site.host | Display host derived from url (scheme, userinfo, port, and path stripped) |
site.main_site.label | Wordmark text; falls back to site.name only when unset – an explicit empty string is honored |
site.main_site.badge_label | Back-to-home chip text; defaults to "Docs" when unset |
site.main_site.cta_label | Call-to-action button text (optional) |
site.main_site.cta_url | Call-to-action target URL (optional) |
Linking Back to a Main Site
A docs (or other satellite) site often fronts a separate product or
marketing site. Declaring that relationship in config.yaml lets the
theme render navigation back to it:
site: main_site: url: https://example.com label: Example # optional; defaults to site.name badge_label: Docs # optional; text of the back-to-home chip cta_label: Get a license # optional; CTA renders only with both cta_* keys cta_url: https://example.com/pricing
The bundled default theme reacts to the block in three places: the
header wordmark links to main_site.url while a small chip
(badge_label, default “Docs”) next to it keeps the way back to the
site’s own home page, an accent-colored button in the header links
cta_label to cta_url, and the footer carries main_site.host –
the host name derived from the URL. Without the block, none of this
renders and the wordmark links to the site’s own home page. In your
own templates, gate on the block the same way:
{% if site.main_site %}
<a href="{{ site.main_site.url }}">{{ site.main_site.host }}</a>
{% endif %}
Page Variables
| Variable | Description |
|---|---|
page.title | Page title |
page.content | Rendered HTML content |
page.date | Publication date |
page.author | Author name |
page.lead | Short summary; falls back to the first paragraph when no lead: is set |
page.tags | List of tags |
page.url | Page URL – what the router keys on. Use it for links within the site and for comparing against the current page |
page.canonical_url | The URL to advertise this page under: use it for <link rel="canonical">, og:url, and anything else naming the page to the outside world. Same as page.url for almost every page |
The two differ for one page. A home page in a home/ directory is reachable
at both /home and the site root, because accent serve resolves the root
through it and accent build writes the root index.html from it. Both
return 200 with the same content, so the page needs a single URL to be
advertised under, and that is the root. Point your canonical tag at
page.canonical_url and the tag will agree with what sitemap.xml lists;
point it at page.url and the two will name different URLs for one page,
which is worse than either on its own – a search engine then has two
contradictory claims and picks between them itself.
Theme Variables
| Variable | Description |
|---|---|
theme.name | Theme name |
theme.assets.css | List of CSS files |
theme.assets.js | List of JS files |
Navigation
| Variable | Description |
|---|---|
pages | All visible pages for navigation |
Taxonomy (Tags)
| Variable | Description |
|---|---|
taxonomy.tags | All tags with name and count, sorted alphabetically |
taxonomy.current_tag | Current tag name (on /tags/{tag} pages only) |
taxonomy.pages | Pages for current tag (on /tags/{tag} pages only) |
Tag index and per-tag listing pages are served at /tags and /tags/{tag}. Create tags.html.jinja and tag.html.jinja templates to customize their appearance. See the Templating Guide for full examples.
Built-in Styling Pipeline
Accent CMS includes a built-in CSS processing pipeline that handles Sass/SCSS compilation, Tailwind-style utility CSS, and production minification – no Node.js required. Configure it in your theme.yaml:
styling: sass: enabled: true utilities: enabled: true scan: - templates/**/*.jinja processing: minify: true
See the Styling Guide for full documentation.
Styling Tips
- Mobile-first - Start with mobile styles, then add breakpoints
- CSS Variables - Use custom properties for easy theming
- Semantic HTML - Use proper elements for accessibility
- Minimize dependencies - Keep your CSS lean and fast
Activating Your Theme
Set your theme in config.yaml:
theme: name: my-theme
Then restart the server to see your changes.