Markdown Checkboxes: Syntax and Render Fixes
Write a Markdown checkbox as - [ ] task or - [x] task, then render it with task list support enabled. Diagnose failures in three places: the source can have a malformed marker, the renderer can leave valid brackets as text, or the application can display a box without saving its state. 17
Classify the failure before changing the Markdown checkbox
Repair malformed markers in the source, literal valid brackets in the renderer, and disappearing ticks in storage. A visible bullet tells you the renderer recognized a list, while a checkbox requires an additional transformation. Rewriting the same valid task line cannot add a missing renderer feature.
| What you see | How to distinguish it | Repair path |
|---|---|---|
| Brackets beside a bullet | Compare the source with - [ ] task; inspect the generated HTML for a checkbox input |
Correct a malformed marker, or enable task list processing if the source matches |
| Brackets inside a table cell or a code block | Inspect whether the source places the marker in a list item, a pipe table, or a fence | Move actionable tasks into a list; keep examples inside fences |
| A box that cannot be changed | Inspect whether the generated input has a disabled attribute | Use source edits for a static document, or an application's supported task controls |
| A tick that disappears after reopening | Compare the saved source before and after the change | Save through an application that writes task state, or edit the source marker |
The first pair of symptoms occurred in our renderer probes. The state checks describe how to diagnose an application's behavior. We did not click through GitHub.com or VS Code. Local HTML output establishes what a converter emitted, not what another product lets a user save. 7
Write task markers inside list items
Put the task marker at the beginning of a list item's text. For a portable starting example, use a hyphen, a space, the bracketed state, another space, and the task label:
- [x] alpha
- [ ] beta
The checked form contains x. The unchecked form contains a space. GFM also accepts uppercase X. An empty bracket pair, spaces around the letter, and a label joined to the closing bracket are different inputs. Our malformed examples stayed as bracketed text even in the local path that rendered the valid example as checkboxes. 17
Task markers need their list container. In our bare-marker sample, brackets at the start of a paragraph stayed text. In the fenced sample, the same task lines stayed code. That behavior is useful in documentation: a code fence presents the characters a reader should copy, while the unfenced list presents the tasks. 7
Nesting adds a list-structure question before it adds a checkbox question. Our nested sample placed a child task under its parent and produced nested checkboxes in Pandoc's GFM path. A child appearing alongside its parent points to indentation or list parsing. A child nested correctly but still showing brackets points to task processing. Inspect the list hierarchy before changing the checked state. 7
A pipe table is a different container. Our table sample produced a table in the table-capable configurations, but its task-looking cell stayed text. For a status table, write a textual status such as Done or Pending. For work someone needs to tick off, put the task in a list beside the table. 7
Numbered tasks also need an implementation-specific check. GFM defines the marker through list items, so a rule that says only hyphens can ever work is too narrow. Yet our local Pandoc GFM path left the numbered sample as brackets while converting the hyphen sample. Use the output from the intended renderer to decide whether numbered tasks fit that workflow. 17
Use a renderer configuration that processes task lists
Use a task-list-capable configuration when valid markers remain literal text. The following paths received identical source text. They demonstrate configured behavior, not a compatibility ranking for every installation of these libraries. 7
CommonMark keeps the task markers as text
Our markdown-it-py CommonMark configuration created the list but left its state markers visible. The preset's rule list does not include task list processing. Changing CSS does not perform the missing task-list parsing step. 2
Output from our basic task-list sample in both CommonMark configurations, unedited:
<ul>
<li>[x] alpha</li>
<li>[ ] beta</li>
</ul>
The CommonMark configuration with breaks=True emitted the same output. In the pinned source, the soft-break renderer chooses between a newline and a line-break element. That option does not register a task parser or replace the bracket marker. 237
| Configuration boundary | Visible consequence |
|---|---|
| CommonMark preset without task processing | List bullets work, bracket markers remain |
| The same preset with the breaks option enabled | Task markers still remain |
Python-Markdown's selected extensions leave brackets intact
Our Python-Markdown configuration enabled tables and fenced code, but did not convert the task markers. Extensions add particular syntax features. Enabling a table feature does not imply that every other extended Markdown feature is active. 47
Output from our basic task-list sample, unedited:
<ul>
<li>[x] alpha</li>
<li>[ ] beta</li>
</ul>
This is a configuration result, not a claim that Python-Markdown cannot support task lists through additional extensions. If an existing publishing pipeline uses it, compare the enabled extension set with the feature the document needs.
| Configuration boundary | Visible consequence |
|---|---|
| Tables and fenced code enabled without a task extension | Tables can render while task markers remain text |
| Task list capability not established | A successful ordinary list is insufficient evidence |
Pandoc GFM renders the basic list but leaves a numbered-list boundary
Our Pandoc GFM path generated checkbox inputs for the basic task list. The output contains a checked input for alpha and an unchecked input for beta. It also wraps each label around its input. 57
Output from our basic task-list sample, unedited:
<ul class="task-list">
<li><label><input type="checkbox" checked="" />alpha</label></li>
<li><label><input type="checkbox" />beta</label></li>
</ul>
The numbered sample exposed a narrower result than a general claim of task list support would suggest. The list remained ordered, but its markers did not become inputs. This is the observed behavior of the tested version and configuration, not evidence that GitHub.com behaves the same way. 7
Output from our numbered task-list sample, unedited:
<ol type="1">
<li>[x] alpha</li>
<li>[ ] beta</li>
</ol>
| Input boundary | Visible consequence in this path |
|---|---|
| Basic or nested hyphen task list | Checkbox inputs appear |
| Numbered list with task markers | Ordered list appears with literal brackets |
| Task-looking content inside a table | Table appears without a checkbox input |
| Malformed markers | Ordinary list items retain the text |
An ordered list that survives while its task markers remain shows that list recognition succeeded. A table that survives without checkbox inputs shows that table support succeeded. Neither success proves that the task transformation ran for that container. 7
Save task state where the document can retain it
Keep task state in the Markdown source or in an application that writes it back. A generated checkbox element defines a displayed control. The HTML fragments above contain no mechanism for editing the original Markdown file. Changing a control in a browser would need a separate save operation to persist as a source edit.
For a static document, replace the space inside the marker with x and save the Markdown. For an application-backed checklist, the application must associate a rendered task with stored content and persist the change. GFM leaves interaction behavior to implementations. GitHub documents tasklists in comments as clickable controls, but that is documented platform behavior, not a result of our local renderer experiment. 16
| Intended use | Where completion belongs |
|---|---|
| A checklist maintained alongside source code | The saved Markdown marker |
| A collaborative checklist in an application | The application's stored task content |
| A static HTML handout | The generated display, unless a separate storage mechanism is provided |
Document conversion adds a source-review step. After PDF to Markdown conversion, compare each task label and its completed or pending state with the original document before turning it into an editable task list. A printed square can be decorative, and changing it into an unchecked task would assign meaning the source may not have. This is a manual cleanup workflow, not a claim that the conversion service implements task list rendering.
For a static checklist, keep readable task markers in the source and use a renderer that handles the required list form. For work that needs saved ticks, use source edits or an application with a save path. Literal brackets call for a syntax or renderer repair; disappearing ticks call for a storage repair.
How we tested
The experiment compares local HTML structure and does not test GitHub.com, VS Code, browser interaction, accessibility behavior, visual styling, or persistence. No Markovo renderer or conversion API participated in these tests.
The checkbox probe ran on 2026-10-19 with Python 3.11.14, markdown-it-py 4.0.0, Python-Markdown 3.10.2, and Pandoc 3.11. It reused the runner's basic checkbox source and added boundary cases. 78
Source file in lab/renderers/ |
Shape represented |
|---|---|
checkbox.md |
Checked and unchecked hyphen items |
checkbox-ordered.md |
The same labels in a numbered list |
checkbox-nested.md |
A parent and an indented child |
checkbox-table.md |
Task-looking text inside a pipe-table cell |
checkbox-malformed.md |
Empty brackets, joined label, and padded state letter |
checkbox-fenced.md |
Task lines inside a Markdown code fence |
checkbox-bare.md |
Brackets in a paragraph without a list marker |
checkbox-uppercase.md |
An uppercase checked marker |
Run the probe from the repository root, using the existing environment:
.venv/bin/python runs/2026-10-19-markovo-markdown-checkbox/lab/checkbox_probe.py
The probe source contains every input and call. It writes HTML as .txt evidence files rather than executing it. The original runner command was .venv/bin/python tools/lab_markdown_render.py --lab runs/2026-10-19-markovo-markdown-checkbox/lab. The checkbox-only rerun preserved unrelated upstream rows. 8
| Path | Exact configuration |
|---|---|
| markdown-it-commonmark | MarkdownIt('commonmark').render(source) |
| markdown-it-breaks | MarkdownIt('commonmark', {'breaks': True}).render(source) |
| python-markdown | markdown.markdown(source, extensions=['tables', 'fenced_code']) |
| pandoc-gfm | pandoc -f gfm -t html, with source passed on standard input |
Counts below come directly from results.json, restricted to relevant samples. Each cell is list items / checkbox inputs. The parser counts HTML start tags, counts checked and disabled attributes on inputs separately, and assigns no visual-quality score. This is a rendering probe, so PDF recall, OCR metrics, and tools/lab_score.py do not apply. 78
| Sample | CommonMark | CommonMark + breaks | Python-Markdown | Pandoc GFM |
|---|---|---|---|---|
| Basic | 2 / 0 | 2 / 0 | 2 / 0 | 2 / 2 |
| Numbered | 2 / 0 | 2 / 0 | 2 / 0 | 2 / 0 |
| Nested | 2 / 0 | 2 / 0 | 2 / 0 | 2 / 2 |
| Table cell | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 |
| Malformed | 3 / 0 | 3 / 0 | 3 / 0 | 3 / 0 |
| Fenced | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 |
| Bare | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 |
| Uppercase | 1 / 0 | 1 / 0 | 1 / 0 | 1 / 1 |
Pandoc's basic and nested samples each contained a single checked input. Its uppercase sample also contained a checked input. None of the emitted inputs had a disabled attribute. The table sample produced a table in Python-Markdown and Pandoc GFM, but not in the CommonMark configurations. Those observations do not establish browser click behavior or storage. 7
Raw outputs use lab/renderers/<sample>--<renderer>.txt. The quoted basic outputs are CommonMark, CommonMark with breaks, Python-Markdown, and Pandoc GFM. The numbered example is Pandoc GFM's raw ordered-list output. The full checkbox count table includes checked, disabled, and table counts.
Source inspection used the pinned CommonMark preset's make() rule list and RendererHTML.softbreak(). Local extracts are the preset and the soft-break function. Pandoc's pinned specFor selects taskListSpec when its task-list extension is enabled. That entry point alone does not establish which list containers the dependency transforms. The numbered-list conclusion comes from the output, without attributing an unverified internal cause. 239


