Group adjacent code fences into a tabbed sample
When two or more code variants show the same operation — bash vs. PowerShell, a csproj property vs. its CLI equivalent, C# vs. F# — a tabbed group lets the audience pick one without scrolling past the others. Author each variant as a normal fenced code block with tabs=true title="..." in the info string and Pennington collapses adjacent matches into a single ARIA tablist. For the info-string grammar, see Code-block argument reference.
Assumptions
- An existing Pennington site rendering markdown (see Create your first Pennington site if not).
- The host wires the default Pennington markdown pipeline, which already enables
UseTabbedCodeBlocksunderAddDocSite,AddBlogSite, or bareAddPennington. - Familiarity with the fence info-string shape (language token plus key/value attributes) — the reference page above covers the grammar.
Tabs and labels
Each H3 below shows the source markdown above the rendered widget. The first tab is active by default; switching tabs reveals the matching panel.
Adjacent fences become tabs
Author two or more fenced blocks back-to-back, each with tabs=true title="..." in the info string. The extension walks the document, finds consecutive FencedCodeBlocks whose tabs attribute is "true", and folds them into one tablist. The title value becomes the tab label; the language token before the attributes still drives syntax highlighting.
````bash tabs=true title="bash"
dotnet add package Pennington
````
````powershell tabs=true title="PowerShell"
Install-Package Pennington
````
````xml tabs=true title="csproj"
<PackageReference Include="Pennington" Version="1.0.0" />
````
dotnet add package Pennington
Install-Package Pennington
<PackageReference Include="Pennington" Version="1.0.0" />
Prose between fences splits the group
The grouping logic only collapses fences that sit next to each other in the block stream. A paragraph, heading, or blank-lined HTML element between two fences splits the group into two separate tablists. To keep one widget, remove the intervening block.
````bash tabs=true title="bash"
echo "first group"
````
A paragraph here ends the first tablist.
````bash tabs=true title="bash"
echo "second group"
````
echo "first group"
A paragraph here ends the first tablist.
echo "second group"
What the renderer emits
The rendered HTML draws its CSS class names from TabbedCodeBlockRenderOptions. The Default instance ships with not-prose on the outer wrapper plus tab-container, tab-list, tab-button, and tab-panel on the nested elements — enough for the MonorailCSS preset to style them without extra work.
/// <summary>
/// Custom container block that holds multiple code blocks rendered as tabs.
/// </summary>
internal sealed class TabbedCodeBlock() : ContainerBlock(null);
/// <summary>
/// Options for customizing the CSS classes used in the tabbed code block renderer.
/// </summary>
public record TabbedCodeBlockRenderOptions
{
/// <summary>Default CSS class configuration used by the tabbed code block renderer.</summary>
public static readonly TabbedCodeBlockRenderOptions Default = new()
{
OuterWrapperCss = "not-prose",
ContainerCss = "tab-container",
TabListCss = "tab-list",
TabButtonCss = "tab-button",
TabPanelCss = "tab-panel",
};
/// <summary>CSS class for the outer wrapper element.</summary>
public required string OuterWrapperCss { get; init; }
/// <summary>CSS classes for the container.</summary>
public required string ContainerCss { get; init; }
/// <summary>CSS classes for the tab list.</summary>
public required string TabListCss { get; init; }
/// <summary>CSS classes for the tab buttons.</summary>
public required string TabButtonCss { get; init; }
/// <summary>CSS classes for the tab panels.</summary>
public required string TabPanelCss { get; init; }
}
To override the class names, set PenningtonOptions.TabbedCodeBlockOptions to a Func<TabbedCodeBlockRenderOptions> returning a modified with expression. The factory replaces the Default shape on the pipeline's single registration of the tabbed extension, so every rendered page picks up the new class names. This works identically on AddPennington, AddDocSite, and AddBlogSite because each surface plumbs the same property through to the pipeline factory.
penn.TabbedCodeBlockOptions = () => TabbedCodeBlockRenderOptions.Default with
{
OuterWrapperCss = "not-prose",
ContainerCss = "lab-tabs",
TabListCss = "lab-tabs-list",
TabButtonCss = "lab-tabs-button",
TabPanelCss = "lab-tabs-panel",
};
Related
- Reference: Markdown extensions catalog — the tabs extension alongside every other non-CommonMark feature
- Reference: Code-block argument reference — the info-string grammar that carries
tabs=true title="..." - Reference: Content components — the Pennington.UI
<Tabs>component for non-code tabsets - Background: Dev mode and build mode share one code path — why the render options flow through one pipeline in both modes