Frontend Theme for Extension Development 

Extension key

theme_extension_development

Package name

sbuerk/theme-extension-development

Version

main

Language

en

Author

sbuerk

License

This document is published under the Open Content License.

Rendered

Thu, 17 Sep 2026 04:52:50 +0000


TYPO3 frontend theme for development purposes: extension development, DDEV based test instances and acceptance tests.


Introduction 

Learn what the extension is for and which TYPO3 and PHP versions are supported.

Installation 

Install the extension in your development or test instance.

Configuration 

Enable the theme for a site and override its templates.

Components 

The component library, the components that need JavaScript and what they do without it.

Changelog 

Overview of the changes per released version.

Introduction 

What does it do? 

The Frontend Theme for Extension Development extension provides a TYPO3 frontend theme for development purposes. Its job is to give a TYPO3 installation a reasonable frontend to look at and to render against, without building a site package for it first.

The situations it is built for, where an extension has to be seen or exercised in a frontend rather than only in a test assertion:

  • Extension development, to click through what an extension actually outputs instead of reading the rendered HTML in a test failure.
  • DDEV based test instances of an extension repository, where a throwaway TYPO3 installation needs a frontend rendering pages, navigation and content elements.
  • Acceptance tests, which need a stable and predictable frontend to drive a browser against.
  • Reproducing an issue in a minimal installation before debugging it.

Core version aware implementations 

Code that has to differ between the supported TYPO3 versions lives below Core13/ and Core14/ in the repository root. Shared code — interfaces, abstract base classes and everything working on both core versions — lives in Classes/.

Only the directory matching the running TYPO3 version is registered in the dependency injection container, so a service asking for an interface always receives the implementation matching the current core version.

Compatibility 

Branch Extension TYPO3 PHP
main 2.x v13.4 / v14.3 8.2 - 8.5
1 1.x v12.4 / v13.4 8.1 - 8.4

This manual documents the main branch. Branch 1 is the maintained line for the previous core version tuple and has a manual of its own; PHP 8.1 is supported there for TYPO3 v12 only, because TYPO3 v13.4 requires PHP 8.2.

Contributing 

Contributions are welcome. The development setup, the quality gates and the commit message rules are described in the CONTRIBUTING.md file of the source repository.

Installation 

The extension has to be installed like any other TYPO3 CMS extension.

Composer mode 

Being a development tool, it usually belongs in require-dev — of the extension repository whose frontend is to be looked at, or of the test instance set up for it:

composer require --dev sbuerk/theme-extension-development
Copied!

A TYPO3 extension required that way is installed and activated exactly like any other one, because typo3/cms-composer-installers makes no distinction between require and require-dev. A deployment installing with composer install --no-dev simply leaves it out, which is the point.

Classic mode 

  1. Get it from the Extension Manager: Switch to the module Admin Tools > Extensions, switch to Get Extensions and search for the extension key theme_extension_development, then import the extension from the repository.
  2. Get it from typo3.org: You can always get the current version from TER by downloading the zip version. Upload the file afterwards in the Extension Manager.

Installing the extension does not render anything yet. The theme still has to be enabled for a site — see Configuration.

Configuration 

Enabling the theme for a site 

The theme ships a site set. A site enables it by depending on that set in its config/sites/<identifier>/config.yaml:

dependencies:
  - sbuerk/theme-extension-development
Copied!

Nothing else is required. The set brings the TypoScript, the page rendering and the stylesheet with it, and no sys_template record is needed. The set itself declares neither dependencies nor settings — everything an integrator changes is a TypoScript constant, and those are described below.

In the backend the same set can be selected under Site Management > Sites in the Sets field of the site.

Installations without site sets 

For an installation that does not use site sets, the theme additionally registers a classic static template. Create a sys_template record on the root page and include Theme Extension Development in Include static (from extensions).

Both paths read the same TypoScript files, so what they deliver is identical.

Templates and stylesheet 

The Fluid paths and the stylesheet are TypoScript constants, so an integrator can render their own templates without editing the theme:

Constant Default
theme.templateRootPath EXT:theme_extension_development/Resources/Private/Templates/
theme.partialRootPath EXT:theme_extension_development/Resources/Private/Partials/
theme.layoutRootPath EXT:theme_extension_development/Resources/Private/Layouts/
theme.stylesheet EXT:theme_extension_development/Resources/Public/Css/theme.css
theme {
    templateRootPath = EXT:my_site_package/Resources/Private/Templates/
    partialRootPath = EXT:my_site_package/Resources/Private/Partials/
    layoutRootPath = EXT:my_site_package/Resources/Private/Layouts/
    stylesheet = EXT:my_site_package/Resources/Public/Css/my-theme.css
}
Copied!

The three paths are those of both content element objects: lib.contentElement, which renders the classic content elements, and lib.themeContentElement, which renders the theme's own - see Important: Theme content elements have their own frame. A root path added to one of the two objects directly reaches only the elements of that object.

The stylesheet is compiled from SCSS sources that ship with the extension, so it can also be rebuilt with different design tokens instead of being replaced — see Feature: Design tokens, with light and dark appearance.

Image width 

The image based elements scale their images to the width the layout gives the content column. That width is a constant, because nothing in TypoScript can read it out of the stylesheet:

theme.media {
    # The width in pixels the gallery is computed for.
    maxGalleryWidth = 1200

    # The same, for an element positioned beside the text.
    maxGalleryWidthInText = 420
}
Copied!

The default matches the 75rem of --theme-content-max-width. Set it too low and images are processed smaller than they are displayed, which shows. Set it far too high and every image is processed at a size no visitor ever sees.

Appearance and palette 

The theme renders in a light and a dark appearance and carries five colour palettes. What is rendered server side — before the display settings in the header can restore a visitor's choice from localStorage — is configured with three constants:

Constant Default Values
theme.appearance.default auto auto, light, dark
theme.appearance.palette neutral neutral, ember, ocean, moss, violet
theme.appearance.contentOutline on on, off

They are written onto the <html> tag as data-theme, data-palette and data-theme-content-outline. Two of those are worth knowing exactly:

  • auto renders no data-theme attribute at all. The appearance is then left to the operating system through color-scheme, which is what light-dark() in the stylesheet resolves against. A palette has no such case, so neutral is still written out.
  • contentOutline draws the labelled outline around every content element. Only off has a rule of its own; on is simply the absence of it. It is a development and staging affordance — a site package rendering for real visitors sets it to off.

All three are defaults. A visitor can change each of them behind the cog at the end of the header - the outline with the Element outlines switch - and the choice is kept in that browser. Reset in the same panel returns to the values configured here: they are also handed to the page template as settings.appearance.default, settings.appearance.palette and settings.appearance.contentOutline, and rendered next to the control for exactly that purpose.

See Feature: Display settings behind a cog button for the display settings, Feature: Appearance switcher for the mechanism behind them and Feature: Design tokens, with light and dark appearance for what the palettes are built from.

Backend layouts and page templates 

The theme ships six backend layouts through page TSconfig, and the layout an editor selects picks the Fluid template the page is rendered with:

Identifier Backend label Columns (colPos) Page template
default Default main (0) Page/Default.html
content Content page stage (2), main (0), footer 1-4 (11-14), footer meta (10) Page/Content.html
content_sidebar Content page with sidebar stage (2), main (0), sidebar (1), footer 1-4 (11-14), footer meta (10) Page/ContentSidebar.html
start Start page stage (2), main (0), footer 1-4 (11-14), footer meta (10) Page/Start.html
styleguide Styleguide unused (999) Page/Styleguide.html
forms Form showcase unused (999) Page/Forms.html

The colPos numbers are a contract, not an implementation detail: the same number means the same slot in every layout, which is what lets an editor change a page's layout without content disappearing. The footer columns and the footer meta row slide down the rootline, so they are edited once on the start page and appear on every page below it.

The mapping is by convention rather than by configuration: the identifier is upper-camel-cased and prefixed with Page/, so a new layout needs a TSconfig file and a template of the matching name and nothing else. A page without a layout, and a page whose layout is TYPO3's built-in [None], both render with Page/Default.html.

See Feature: Backend layouts decide the page template.

Content elements 

The theme brings its own content element rendering and does not depend on fluid_styled_content — that extension is not required here. An installation that has it anyway enables the bridge set instead, see Feature: A bridge to fluid_styled_content. What the theme's own rendering covers:

The fields of the Appearance tab - frame, space before and after - and the alignment and style of the header change how an element looks, and the page TSconfig of the theme takes the ones it does not render out of the form - see Feature: Appearance fields of content elements.

Nothing an editor can create is left without a rendering definition, and a test asserts exactly that: it walks the content types registered in TCA and fails if the core's "no rendering definition" notice appears for any of them.

Demo content 

A page tree to look at is imported rather than built by hand. The extension ships it as a seed set of sbuerk/data-factory, which is suggested rather than required:

composer require --dev sbuerk/data-factory
vendor/bin/typo3 data-factory:import theme-demo
Copied!

The set seeds a start page, pages for typography and media, one page deliberately without a backend layout, a /elements branch carrying every content type, and a /styleguide page rendering the whole component library — see Feature: A seeded showcase of every element and Feature: A styleguide page. Its records declare their uids and point at each other by them, so it is imported into an installation where those uids are free. The set declares no site configuration: create one with root page 1 afterwards, which is also what makes the tree answer in the frontend. data-factory:import --help lists every option.

Components 

The stylesheet the theme ships is a component library. Every class carries a theme- prefix, and every component states the markup it expects in the header comment of its own SCSS file below Resources/Private/Scss/. That comment is the contract a template is written against; this page is the overview. The /styleguide page of the seeded demo tree renders every component, in the appearance and palette currently selected - see Feature: A styleguide page.

Every component reads its values through custom properties with a fallback, so a site package re-themes one component or all of them from its own CSS, without rebuilding the SCSS - see Feature: Component library.

Every state the library draws in colour - a selected tab, a pressed toggle - also survives forced colours, the high contrast themes of the operating system, where it is drawn in the system highlight colour instead.

What the library contains 

Group Components
Content Accordion, alert, author, badge, card - in a grid, with a column count or as a row that scrolls sideways -, code block, description list - with a divided variant for key and value - call to action, divider, feature and feature grid, figure, file list, hero, list, list group, media object, panel, pricing, quote, stats, steps, table, teaser, timeline, and the text roles display, lead and eyebrow
Data display Avatar and avatar group, tag list, progress bar, meter
Actions Button - with a link and an icon variant, pressed and busy states, and an attached button group - and the close button
Icons The solid icons of Font Awesome Free, inline, see Icons
Interactive Tabs, dialog, tooltip, toggletip
Forms Controls, field, input group, choice group, validation
Navigation and page Main and sub navigation, breadcrumb, pagination, content menu - which is also the table of contents of a page - language menu, dropdown, skip link, gallery, the content element wrapper, site header and site footer

Icons 

The theme ships the complete solid style of Font Awesome Free 7.3.1 - 2001 icons - below Resources/Public/Icons/FontAwesome/Solid/, and draws every icon of its own from it. A template renders one with the ViewHelper <theme:icon>, by the file name of the icon without .svg:

<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
      xmlns:theme="http://typo3.org/ns/SBUERK/ThemeExtensionDevelopment/ViewHelpers"
      data-namespace-typo3-fluid="true">

<theme:icon name="circle-info" />
<theme:icon name="circle-info" label="Information" />
Copied!

The icon is inlined as SVG: no webfont, no CDN and no request is involved. It is as large as the text around it and takes the colour of that text. Without label it is decoration and hidden from screen readers - the right choice next to text or inside a button that has a name of its own. With label it is an image with that name. An icon name that does not exist stops the rendering with an exception rather than leaving a gap.

The optional class argument adds classes to theme-icon . A component that gives icons a fixed size sets the custom property --theme-icon-size on the element around them.

The icons are licensed under CC BY 4.0; the npm package they come from declares CC-BY-4.0 AND OFL-1.1 AND MIT, of which only the icons ship here. The licence of the package and an attribution ship next to them, in Resources/Public/Icons/FontAwesome/LICENSE.txt and Resources/Public/Icons/FontAwesome/ATTRIBUTION.txt. Every icon file keeps the attribution comment it comes with, and so does every icon on a rendered page. The extension itself stays GPL-2.0-or-later.

In the backend, an editor picks an icon from a select list grouped by the categories of Font Awesome, with a grid of the icons under it; page TSconfig keepItems narrows the list per field - see Feature: Editors pick an icon by name.

Components that need JavaScript 

Three components need the theme's script, Resources/Public/JavaScript/theme.js, which the page includes as a module. Each of them stays usable without it - with JavaScript switched off, and with JavaScript on but the script failing to load. The dialog opener follows the data-js attribute that the theme's inline head script sets on the <html> element before the page is painted, the same marker the main navigation's toggle depends on. The tabs follow a marker of their own, data-theme-tabs-bound, which only the theme's script sets on the tab group once it has bound it.

Component With JavaScript Without JavaScript
Tabs The WAI-ARIA tabs pattern: the arrow keys, Home and End move between the tabs, one panel is shown at a time. No tab list. Every panel is shown, each under its own heading, and none of them claims to be a tab panel.
Dialog A button carrying data-theme-dialog-open opens the <dialog> of that id as a modal. A click on the backdrop closes it, and focus returns to the button. The button is not shown, and the dialog stays closed.
Tooltip Escape hides the tooltip without moving focus. Shown on hover and on focus, but Escape does not hide it.

Tabs 

The first tab is the selected one, and a panel carries nothing but its class and its id in the markup. Hiding the panels that are not selected is the script's job, and so are the tab panel role, the label and the tab stop of each panel, so a page without the script shows every panel as plain content. Each panel repeats the label of its tab in a theme-tabs__heading, which labels the panel when there are no tabs to do it.

<div class="theme-tabs">
    <div class="theme-tabs__list" role="tablist" aria-label="Delivery">
        <button class="theme-tabs__tab" type="button" role="tab" id="t-set"
                aria-selected="true" aria-controls="t-set-panel">Site set</button>
        <button class="theme-tabs__tab" type="button" role="tab" id="t-static"
                aria-selected="false" aria-controls="t-static-panel" tabindex="-1">Static include</button>
    </div>
    <div class="theme-tabs__panel" id="t-set-panel">
        <h3 class="theme-tabs__heading">Site set</h3></div>
    <div class="theme-tabs__panel" id="t-static-panel">
        <h3 class="theme-tabs__heading">Static include</h3></div>
</div>
Copied!

Dialog 

A native <dialog>: the browser keeps focus inside it, makes the page behind it inert, and closes it on Escape. The <form method="dialog"> around its content closes it with no script at all. Because nothing can open it without JavaScript, a dialog is for content a page can do without - a confirmation for an action that needs the script anyway.

<button class="theme-button" type="button" aria-haspopup="dialog"
        data-theme-dialog-open="d-reseed">Reseed</button>

<dialog class="theme-dialog" id="d-reseed" aria-labelledby="d-reseed-title">
    <form method="dialog">
        <div class="theme-dialog__header">
            <h2 class="theme-dialog__title" id="d-reseed-title">Reseed the instance?</h2>
            <button class="theme-close" value="cancel" aria-label="Close"><theme:icon name="xmark" /></button>
        </div>
        <div class="theme-dialog__body"></div>
        <div class="theme-dialog__footer">
            <button class="theme-button theme-button--ghost" value="cancel">Cancel</button>
            <button class="theme-button theme-button--danger" value="confirm">Reseed</button>
        </div>
    </form>
</dialog>
Copied!

Tooltip 

A short description of a control that already has a name. The trigger points at the bubble with aria-describedby, which is what a screen reader announces. The bubble sits above its trigger and is not moved to stay inside the viewport, so a trigger at the very edge of the viewport is a placement to avoid.

<span class="theme-tooltip">
    <button class="theme-button theme-button--ghost theme-button--icon" type="button"
            aria-label="Element outlines" aria-describedby="tt-outline"></button>
    <span class="theme-tooltip__bubble" role="tooltip" id="tt-outline"></span>
</span>
Copied!

Headings and text roles 

Heading levels four to six have a step of their own. <h4> is the body size at the bold weight; <h5> and <h6> are smaller, in capitals and with a wider letter spacing, and <h6> is in the secondary text colour. The titles of the hero and the teaser, which follow the heading level an editor picks, look the same on every level.

Three text roles are classes rather than elements, so they go on whatever element the document outline asks for:

Class For
.theme-display The one heading on a landing page that is set larger than a page title. Three sizes: --1 , --2 - the size of the bare class - and --3 . All three are the size of a page title on a phone and grow to 68, 54 and 43 pixels on a wide window.
.theme-lead The paragraph that opens a page.
.theme-eyebrow The short label above a heading.

Rich text needs no class for the rest of the typography:

  • A <small> inside a heading is a secondary line, set smaller and lighter than the heading: <h2>Release notes <small>for 2.0</small></h2>.
  • Headings balance their lines when they wrap, paragraphs avoid a single word on the last line.
  • A key combination is a <kbd> holding one <kbd> per key; only the keys are framed.
  • Quotation marks follow the language of the quotation: English by default, the German and the French pairs for lang="de" and lang="fr".
  • German text is hyphenated, where the browser has a German dictionary. Other languages are not.
  • Text set right to left with dir="rtl" mirrors its indents, list markers and quotation rule.

Layouts of content elements 

Several content elements offer a choice of how they look. Each value maps onto a modifier of the component the element renders, and a value the theme does not know renders the element without one.

Content element Field Values
Text Layout Running text; Columns - .theme-text--columns , two columns of at least 30 characters, one on a phone - see Feature: Text in columns
File Links Display file/icon/thumbnail The file names alone - .theme-file-list ; with the icon of the file type - --icon ; with a thumbnail, or the icon where there is none - --preview . See Feature: File type icons in the file list
Hero, Hero, small, Hero, text only Layout, Eyebrow The image at the start - the default; at the end - .theme-hero--image-end ; centred - --centred ; a screenshot cut off at the bottom - --screenshot ; cropped at two edges - --bordered . The eyebrow is .theme-hero__eyebrow . See Feature: Layouts and an eyebrow for the heroes
Call to action Tone, Width Surface, accent tint - .theme-cta--accent , the other appearance - --inverse , placeholder - --placeholder ; a box - --boxed , or a band - --band . See Feature: Call to action content element
Testimonial Style A rule at the start - the default; a pull quote - .theme-quote--pull ; centred - --centred , both with a quotation mark. See Feature: Pull quote and centred testimonial style
Card group Layout, Columns A grid - the default; one row that scrolls sideways - .theme-card-grid--scroller ; a wall of columns the cards stack into - --wall , which fills column by column. Two, three or four columns - --columns-2 to --columns-4 . See Feature: Card group, timeline and teaser list and Feature: A wall layout for the card group
Split tiles Layout, and Tone per tile The first image at the start of the line - the default; at the end - .theme-split-tiles--reversed . The side alternates from tile to tile by itself. Each tile takes a tone - .theme-split-tiles__item--accent , --inverse , --placeholder . See Feature: Carousel and split tiles
Carousel Caption per slide Below the image - the default; above it - .theme-carousel__caption--above ; over its foot - --overlay . See Feature: Carousel and split tiles

Tables 

The Table class of the table content element chooses the look of the table. The core offers Striped and Bordered; the theme adds the rest of what its table component draws:

Table class Look
Default Rows separated by a hairline, the header row on a tint
striped Every other row on the tint
bordered A hairline between the columns as well
striped-columns Every other column on the tint
hover The row under the pointer is highlighted
borderless No rules at all
compact Half the cell padding
sticky-header The header row stays in view while a tall table scrolls

Each value becomes the modifier .theme-table--<value> . A site package adds its own value with page TSconfig - TCEFORM.tt_content.table_class.addItems - and styles the class it produces; removeItems hides one of the theme's.

Alerts 

Six kinds, as a modifier of .theme-alert . The role belongs in the markup, and it is not the same for every kind:

Modifier For role
--info Information; also the look of the bare class status
--success Something finished status
--warning Something needs attention alert
--danger Something failed alert
--note An aside to the text around it note, or none
--tip A recommendation. Its colour follows the selected palette. note, or none

status and alert are live regions, which announce a change. A note or a tip does not change, so it is never one.

Changelog 

Every notable change to the Frontend Theme for Extension Development extension is documented here, grouped by version and change type.

2.0 Changes 

Table of contents

Breaking Changes 

Features 

Deprecation 

Important 

Feature: Appearance switcher 

Description 

The theme now ships the script and the control that let a visitor change appearance, palette, and the main menu - the piece Feature: Navigation shipped without. Three new TypoScript constants under theme.appearance, in Configuration/TypoScript/Appearance.typoscript, set the server-rendered default; an inline script and Resources/Public/JavaScript/theme.js apply a visitor's stored choice and operate the controls from there.

Constant Values Default
theme.appearance.default auto, light, dark auto
theme.appearance.palette neutral, ember, ocean, moss, violet neutral
theme.appearance.contentOutline on, off on

These are constants, not Site Settings. A constant is read identically by the site set and by the classic sys_template static include, which is the property this theme is built around; a settings.definitions.yaml would only serve the set and introduce a second source of truth for the same value. A site package that wants these editable in the Site Settings UI can still declare them in its own set.

Server-rendered attributes 

config.htmlTag.attributes.* writes every value onto the <html> tag raw - RequestHandler::generateHtmlTag() only applies stdWrap when a matching . sub-array is configured, and none is here - so a constant is the only thing that can go on this path, never a cObject:

config.htmlTag.attributes.data-palette = {$theme.appearance.palette}
config.htmlTag.attributes.data-theme-content-outline = {$theme.appearance.contentOutline}

["{$theme.appearance.default}" != "auto"]
    config.htmlTag.attributes.data-theme = {$theme.appearance.default}
[END]
Copied!

data-theme is behind that condition rather than assigned directly because auto means the absence of the attribute, not a value for it - no selector matches data-theme="auto", and rendering it would look like a working default while doing nothing.

The no-flash script 

An inline script in the document head applies a stored appearance and palette before first paint. It is emitted through page.headerData rather than f:asset.script, because the asset collector may move a script to the end of the body, and a stored dark appearance would then paint light first - the exact flash this script exists to prevent.

It sets data-js on the root first and unconditionally, then reads localStorage for the stored appearance and palette, each inside its own try/catch. localStorage throws rather than returning null in Safari's private mode and when cookies are blocked, and an uncaught throw would abort the script before the marker was set - which is why the marker comes first, not after. Losing it costs more than a missed colour choice: data-js is also what the navigation collapse and the switcher's visibility are gated behind, so a page that lost it keeps the always-expanded navigation and a permanently hidden switcher for the rest of that visit.

The switcher itself is hidden until data-js is set, for the same reason: a control that cannot work yet is a visible promise the page cannot keep, not a degraded one.

The module script 

Resources/Public/JavaScript/theme.js, loaded with page.includeJSFooter and .type = module. A module is deferred by specification, so it needs no defer attribute, and it runs in the footer rather than the head because it only wires up listeners on elements that already exist by then. It owns the appearance control, the palette control, and - now that its script has arrived - the main menu toggle that Feature: Navigation shipped inert.

The file uses no optional chaining (?.) anywhere. A browser that does not recognise type="module" skips the element unparsed and can never fail on syntax inside it, but "recognises modules" and "supports optional chaining" are not the same floor - module support landed 2017-2018, optional chaining in 2020 - and a syntax error anywhere in a module aborts the whole file, with no per-statement fallback the way a classic script has. Plain if checks cost nothing and remove that failure mode entirely.

Palette swatches 

Each palette button in the switcher carries a small swatch in that palette's primary colour. The swatch colour is the one duplicated colour in the stylesheet: every palette lives entirely inside its own [data-palette='…'] selector, and CSS has no mechanism to ask what a custom property would resolve to under a different attribute value, so the swatch cannot reference the token live. ComponentLibraryTest::everyPaletteHasASwatchWithItsOwnColours is what keeps the copy from drifting out of step with abstracts/_palettes.scss.

Impact 

No cookie is set anywhere in this mechanism, and nothing is persisted server side. localStorage is the whole story - two keys, theme-appearance and theme-palette - which is also why there is no consent question to design around: the choice never leaves the browser it was made in.

The main menu toggle, shipped inert in Feature: Navigation, is now fully wired: clicking it flips aria-expanded, Escape and an outside click close the menu again. Nothing about the navigation's no-JavaScript layout changes - the collapse remains gated behind data-js, exactly as before.

Feature: Audio and video in Text & Media 

Description 

The Text & Media element reads the Media field, which takes a video or an audio file as well as an image. Until now the theme rendered every file in it as an image, which a video is not. Both now render as what they are: a native player with the browser's own controls, keyboard handling and full screen button.

A new field, Captions, holds WebVTT files:

launch.mp4    the video
launch.vtt    its captions
Copied!

A caption file belongs to the media file of the same name. Upload both, put the video in Media and the caption file in Captions, and the player offers the track in its own menu. Pairing them by the order of the two lists was the alternative and was rejected: an image added in the middle of Media would silently re-point every caption after it, and nothing on the page would look wrong.

Why the theme writes the tag 

TYPO3 renders a media file through AudioTagRenderer and VideoTagRenderer, which f:media dispatches to. Both emit <video controls><source …></video> and neither has any notion of a text track - there is no track in either class, on TYPO3 v13.4 or v14.3. A caption track is the one part of a media element that is not decoration, so the theme writes the tag itself and leaves those renderers to whoever wants their defaults.

Impact 

The database analyzer adds one column to tt_content, tx_theme_captions, a file field restricted to vtt. It is shown on Text & Media only - the other two media elements read the image field, which takes images alone.

The extension adds vtt to $GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'] in ext_localconf.php, appended and only where it is not already there. TYPO3 ships srt - the other subtitle format - in that list and vtt in none of the three lists a file may be uploaded under, so without it an editor uploading a caption file gets Resource consistency check failed and the field can never be filled. vtt is deliberately not added to mediafile_ext, which is what the Media field accepts: a caption file is not a medium of the element, it belongs to one.

The stylesheet gains the .theme-media component - see Components. A gallery item that is a player carries it beside .theme-figure, so the caption stays the gallery's own.

The player carries preload="metadata": a gallery may hold several of them, and none is what the visitor came for. A video is letterboxed into a 16:9 box until its own dimensions are known, because FAL records no width or height for a video file and the gallery would otherwise reserve a box of no height for it.

An installation that has put a video into Media before this change was showing a broken image in its place; it now shows a player, with no change to the record.

Feature: Backend layouts decide the page template 

Description 

The theme ships six backend layouts, and the layout selected on a page now decides which template renders it. Until now every page rendered the same file.

Layout Template Content areas
Default Page/Default.html main
Content page Page/Content.html stage, main, four footer columns, footermeta
Content page with sidebar Page/ContentSidebar.html the same, plus sidebar beside main
Start page Page/Start.html the same as Content page
Styleguide Page/Styleguide.html none - it renders components directly
Form showcase Page/Forms.html none - it renders a form directly, see Feature: Form showcase

The column numbers are the ones typo3/theme-camino uses, so content is portable between the two themes.

Registration 

The layouts are page TSconfig, in Configuration/page.tsconfig, which TYPO3 auto-loads from every package since v12.0 (forge#96614). No registration call and no database record is involved, and it applies whether the theme is delivered through its site set or through the classic sys_template static include - the alternatives would each only work for one of those.

Overriding one layout means overriding one file:

mod.web_layout.BackendLayouts.content.config.backend_layout.rows.2.columns.1.colPos = 5
Copied!

Content areas 

Each column is its own TypoScript object, so a site package can replace one without touching the rest:

<f:cObject typoscriptObjectPath="lib.content.main" />
Copied!

The four footer columns and the footer meta row carry slide = -1. Footer content is therefore edited once on the site root and inherited by every page below it - what typo3/theme-camino gets from slideMode = slide, without depending on typo3/cms-fluid-styled-content .

Impact 

The template is resolved with data = pagelayout, not field = backend_layout. The getter resolves through PageLayoutResolver, which falls back to the first ancestor's backend_layout_next_level when a page carries no layout of its own. Reading the field directly would ignore that, and every sub-page of a configured parent would silently render the wrong template.

Two edges of that inheritance are worth knowing, and both are covered by Tests/Functional/BackendLayoutRenderingTest.php:

  • A page's own backend_layout_next_level applies to its children and never to itself - the resolver removes the current page from the rootline before searching.
  • Choosing TYPO3's built-in [None] option in the page properties resolves to the literal identifier none, which is not empty. It is mapped back to default; without that it would ask for a Page/None.html that no theme ships and end the request in an exception.

Every column declares an identifier as well as a name and a colPos. TYPO3 v14 raises a deprecation for a column without one and will throw in v15; v13 ignores it, so one spelling serves both versions.

Feature: Layouts for the bullet list 

Description 

The Bullet List content element renders the list components of the theme. Its Layout field, in the Appearance tab, is offered for this element again and picks the look of an unordered or an ordered list:

Layout Renders
Bullets or numbers The list with the markers of the element baseline.
Check marks A check mark in front of every item.
Icons The icon picked in the new field Icon in front of every item. The field offers the icons of the theme's icon picker; with no icon picked the list keeps its markers.
Inline The items on one line that wraps, with a hairline between them.

A definition list is rendered with its terms and descriptions side by side on a wide screen, stacked on a narrow one, whatever the layout says. See Important: A definition list reads "term|description" lines for how its lines are read.

Layout stays hidden for every other content element: the theme renders it for the bullet list only.

Impact 

Every bullet list renders <ul class="theme-list"> or <ol class="theme-list">, with the modifier of the chosen layout. A site package that styles the bare list of a bullet list element gets the component instead; its own rules for .theme-content-element--bullets ul still apply, and win where they are more specific.

A site package can relabel or remove the layouts in page TSconfig, below TCEFORM.tt_content.layout.types.bullets.

Feature: Call to action content element 

Description 

A new content element in the Theme group, Call to action, renders a heading, a short rich text, an optional large icon and up to two links as a band or a box - the jumbotron of other themes.

Field Values
Tone Surface, the default; Accent tint, the tint of the accent band of the Appearance tab; Inverse, the other appearance - dark on a light page, light on a dark one; Placeholder, no fill and a dashed frame, for content that does not exist yet.
Width A box, centred in the column, the default, or A band across the whole column.
Icon An icon of the theme's icon picker above the heading.
Link and Second link Two links, each with a label, a style and an icon, like the link of a hero. The second link defaults to the outlined style.

The element is available to editors once their backend group grants the content type Call to action.

Impact 

The element renders <section class="theme-cta"> with the modifiers --boxed or --band and --accent , --inverse or --placeholder . It is rendered by lib.themeContentElement, like every content element of the theme.

Feature: Card group, timeline and teaser list 

Description 

Three more content elements of the theme's own, in the Theme group of the Create new content element wizard. All three take their items from the inline list the other list based elements of the theme use:

Content element Renders
Card group Cards with an image, a title, a subtitle, a text and a button, in a grid or in one row that scrolls sideways.
Timeline Dated entries on a line, oldest or newest first.
Teaser list Rows with a small round image, a title, a text, a date and meta data. The whole row is one link.

Card group 

Layout offers Grid and Scroller: one row that scrolls sideways, and Columns the most cards side by side: two, three or four. A narrower screen shows fewer, a phone one; the scroller keeps the number as the cards in view and always shows the edge of the next one. The scroller needs no JavaScript: it scrolls by touch, trackpad, wheel and scroll bar, and with the arrow keys once it has focus - it is a region with a tab stop, named after the heading of the element.

Each card has a link with a label, an icon and a Link style - the same four styles the link of a hero or a teaser offers - and renders it as a button.

Timeline 

Every entry has a date, a title, a text, and an optional icon and image; an entry with an icon shows it on the line in place of the dot. The entries are sorted by their date - Oldest first or Newest first in Order - and the order of the list in the backend decides only between entries of the same date. The date is written out in the language of the site, "12 January 2026" in English, and marked up as a machine readable <time>.

Teaser list 

Every row has a title, which is its link, a text, an image, a date and a line of meta data such as a reading time. A row without a link is shown without one.

Impact 

Integrators get three new content types to grant to editor groups: theme_card_group, theme_timeline and theme_teaser_list. The database analyzer adds one column to tt_content, tx_theme_sort_direction, and three to tx_theme_list_item: date, meta and link_variant. The card group arranges its cards by the tx_theme_columns of the features, and shows the subheader of an item; the timeline shows its icon. The field Layout of the Appearance tab stays disabled for every other content type; the card group shows it on its own tab.

A site package overriding Templates/ContentElements/ finds the three templates there as ThemeCardGroup.html, ThemeTimeline.html and ThemeTeaserList.html. The stylesheet gains the timeline and the list group component, and the card grid its column counts and the scroller - see Components.

Feature: A wall layout for the card group 

Description 

The Card group content element offers a third arrangement beside the grid and the scroller: Wall: columns the cards stack into, filled top to bottom.

In the grid the cards sit in rows, and every card in a row is as tall as the tallest one in it, which leaves a ragged edge at the foot of the group when the texts are of different lengths. In the wall each card keeps its own height and the next one starts where it ends, so the columns fill evenly.

Columns works as it does for the grid: it is the most columns the wall takes, fewer where there is no room for them and one on a phone.

Impact 

A card group with the new layout renders <ul class="theme-card-grid theme-card-grid--wall"> with the column modifier it already carried. The modifier is new in the component library, on .theme-card-grid , and joins --wide , --narrow , --columns-2 to --columns-4 and --scroller ; all of those are unchanged, and --wide and --narrow move the column width of a wall exactly as they move it for a grid.

Existing card groups are untouched: the grid remains the default, and no stored record changes meaning.

A site package can relabel or remove the arrangements in page TSconfig, below TCEFORM.tt_content.layout.types.theme_card_group.

Feature: Component library 

Description 

The theme now ships a component library built entirely from the design tokens: an element baseline covering everything a rich text editor can emit, native form controls with their validation states, and the components a page is composed from - navigation, breadcrumb, hero, teaser, card, alert, badge, accordion, quote, table, pagination, gallery, and the page chrome.

There is no CSS framework. No Bootstrap, no Tailwind, no reset library, and no external request of any kind.

Class names carry a theme- prefix. That is not decoration: this theme exists to host other extensions while they are being developed, those extensions bring their own stylesheets, and an unprefixed .button or .card would collide with them.

Component tokens 

Each component declares its own token layer, every entry falling back to a global token and then to a literal:

.theme-card {
    --theme-card-background: var(--theme-color-surface, #f4f6fa);

    background-color: var(--theme-card-background);
}
Copied!

A site package can therefore re-theme globally by moving --theme-color-surface , or surgically by moving --theme-card-background , without touching the other. A modifier re-points a token rather than restating a property.

The fallback literal appears once per component rather than once per declaration, which keeps abstracts/_tokens.scss the single source of truth. It also makes a component portable into a shadow root: custom properties inherit across a shadow boundary, so only the token file has to live in the outer document.

The CType outline 

Every content element is wrapped in .theme-content-element , which draws a dashed outline and a small chip naming its CType. It exists so that the column and element boundaries a backend layout produces are visible without opening the page module.

It is a development aid, and it is switched off wholesale with one attribute:

<html data-theme-content-outline="off">
Copied!

A single element opts out with .theme-content-element--plain .

Impact 

Nothing renders differently yet: the Fluid templates that emit this markup are not part of this release. What ships is the stylesheet those templates will be written against, and the markup contract each component documents in its own file header.

The compiled stylesheet grows from roughly 6 kB to roughly 54 kB, uncompressed and before transport compression.

Feature: Appearance fields of content elements 

Description 

The fields of the Appearance tab and of the header palette now change how a content element looks. Before, an editor could set them and nothing happened.

Field Values Effect
Frame Default, No frame, Surface band, Raised band, Accent band, Inverse band A band fills the element and draws a hairline around it. "No frame" removes the inner padding, for an element that brings its own box.
Space before, Space after Extra small to Extra large The space above and below the element, on the spacing scale of the theme.
Alignment of the header Default, Center, Right, Left The header at the centre, the end or the start of the line. Right and left follow the direction of the text.
Header style As its level, Display, Like heading 1 to 5 How large the header looks. Its level in the outline of the page stays the one Type sets.

Header style is a new field, next to the header level.

The four bands 

  • Surface band and Raised band fill the element with the two surface colours of the theme.
  • Accent band is a light tint of the primary colour of the active palette, so it changes with the palette.
  • Inverse band shows the element in the other appearance: dark on a light page and light on a dark one. Everything inside it changes with it, links and buttons included, so it stays as readable as the rest of the page.

The labelled outline of the development mode is drawn on top of a band and is switched off as before. The band stays.

Impact 

The theme sets page TSconfig for the whole installation. The frames the theme does not draw — the rulers and indents of the core list — are removed from Frame, and three fields are taken out of the form until the theme renders them:

Alignment and Header style are hidden on the hero elements, the media teaser and the testimonial, which show their title in a component of their own.

An element that still holds one of the removed frames is rendered without a frame.

This page TSconfig comes from Configuration/page.tsconfig of the extension, which TYPO3 loads for every page tree of the installation, the same way the theme's backend layouts are registered. So it also reaches a site that does not use the theme. That site gets its own fields back with its own page TSconfig, which the core loads after that of the extensions. The TSconfig can come from the site's set, from config/sites/<site>/page.tsconfig, or from the Page TSconfig field of its root page:

TCEFORM.tt_content {
    frame_class.removeItems >
    frame_class.addItems >
    layout.disabled = 0
    sectionIndex.disabled = 0
    linkToTop.disabled = 0
}
Copied!

A site that does use the theme can offer a removed frame again the same way. It then has to style that class itself.

Feature: Content element rendering 

Description 

The theme renders content elements itself, without depending on fluid_styled_content.

Two content elements are rendered:

Content element Rendered
Header The heading, at the level chosen in Type. The Hidden option of that field is honoured.
Text The heading and the rich text of the element.

Both are wrapped in an element frame carrying the familiar c<uid> anchor, so links to a content element keep working.

Overriding the templates 

The element templates live below Resources/Private/Templates/ContentElements/ and use the same Fluid paths as the page templates, so the constants under theme. redirect them together - see Configuration.

The rendering definition itself is the TypoScript object lib.contentElement, which fills the role the object of the same name has in fluid_styled_content.

Feature: Core content elements 

Description 

The theme now renders the rest of the classic content element set:

Content element Rendered
Text & Images Images, positioned beside Text.
Text & Media The same, from the Media field, which is not restricted to images.
Bullet List Text as a list, unordered, ordered or a definition list, per Type of list.
Table Text as a table, honouring caption, delimiter, enclosure, header position and footer.
File Links The Files and Collections fields as a file list - name, optional size, optional thumbnail.
Insert Records The referenced records, rendered exactly as they render on their own.
Divider A horizontal rule.
HTML Text, unescaped.

Together with Header, Text and Images (Feature: Content element rendering, Feature: Rendering of the Images element), every classic content element EXT:frontend registers now renders, except the eleven menu elements - tracked separately, they need a MenuProcessor configured per menu type rather than only a template.

No TCA of this extension's own is added for any of them. Every one of these elements was already creatable in the backend before this change - their TCA comes from EXT:frontend, on TYPO3 v13.4 and v14 alike, and fluid_styled_content is not a dependency of this theme on either version - so what changes is only that they now render instead of TYPO3's own "no rendering definition" notice.

Two elements needed a decision 

Table 

Text for Table is delimited text, and the delimiter and enclosure fields are stored as TCA character codes, not characters. Neither core data processor is enough on its own: SplitProcessor splits on one delimiter into a flat list, with no nesting and no quoting; CommaSeparatedValueProcessor is built for exactly this field but has no stdWrap property that turns a numeric code into a character, and does not shape a header row, a header column or a footer row.

SBUERKThemeExtensionDevelopmentDataProcessingTableProcessor does the decode and the shaping. One PHP detail is worth knowing if this class is ever touched: the enclosure field's own default is "None" (code 0), and PHP's fgetcsv() throws a ValueError when handed an empty enclosure string - which is what the TYPO3 backend's own table wizard falls back to for that same "None" option. chr(0) is used instead, so the field's default configuration does not throw.

Insert Records 

Insert Records renders other content elements, through the core's RECORDS cObject configured with conf.tt_content =< tt_content - a referenced record renders through the very same object this theme builds for every content element, including, if it is itself an Insert Records element, going through this same branch again.

Escaping 

Table cells, bullet items, the table caption and file names/descriptions are plain Fluid interpolation, HTML-escaped. Text & Images and Text & Media run their Text field through f:format.html (the RTE parse), the same as the existing Text element. HTML is the one deliberate exception: f:format.raw, completely unescaped - not f:format.html, which would parse it and change it. Access to HTML is restricted the same way as every CType: the field carries authMode = explicitAllow, and it is a site administration decision, not something this theme enforces, which backend groups are actually granted it.

Impact 

An installation using the theme no longer renders the TYPO3 "no rendering definition" notice for any of the elements listed above. Tests/Functional/CoreContentElementRenderingTest.php sweeps every covered CType and fails if any of them regresses to the notice.

The markup is generated from the templates below Resources/Private/Templates/ContentElements/, redirected together with every other template through the Fluid path constants under theme. - see Configuration. See docs/architecture/content-elements.md in the developer documentation for the full coverage table, the bullets_type/table_*/uploads_type field-by-field reasoning, and the recursion finding above.

Feature: Avatar, tag, progress and meter components 

Description 

The component library gains the components that show a person or a value rather than a passage of content, pure HTML and CSS, flat and framed like the rest of the library:

  • Avatar - a portrait in a circle or a square, in three sizes, with the initials of the person where there is no portrait, and a group of avatars that overlap.
  • Tag list - keywords, plain or linked, optionally with an icon before the label. A linked tag is underlined, so it is told apart from a plain one by more than its colour.
  • Progress and meter - the native <progress> and <meter>, labelled like a control and followed by their value in words. The edge of the track is drawn in the strong border colour, which reaches 3:1 against every background. A meter draws its three regions in the success, the warning and the danger colour, and its value text says what the region means.

The description list takes a modifier, .theme-dl--divided , for pairs of key and value: a hairline between one term and the next.

What each of them expects is on Components, and the styleguide shows all of them in a section of its own, "Data display".

Impact 

Everything is new and changes nothing that renders today. The styleguide has a twelfth section. No design token is added.

Feature: Design tokens, with light and dark appearance 

Description 

The stylesheet is built from a documented set of design tokens covering typography, colour, spacing, border radius, borders, focus, controls, stacking, motion and layout width. Every one of them is a CSS custom property.

Every colour is declared once and carries both appearances, using the CSS light-dark() function:

:root {
    color-scheme: light dark;
    --theme-color-background: light-dark(#ffffff, #0f1319);
}
Copied!

Appearance 

The theme follows the appearance the visitor asked for. The operating system decides by default, and an explicit request on the root element overrules it in both directions:

<html data-theme="dark">
<html data-theme="light">
Copied!
:root                     { color-scheme: light dark; }
:root[data-theme='light'] { color-scheme: light; }
:root[data-theme='dark']  { color-scheme: dark; }
Copied!

That is the whole mechanism. It works because light-dark() resolves against the used value of color-scheme , so switching one property switches every colour. It does not work by influencing what prefers-color-scheme matches, which is tied to the operating system and cannot be changed from CSS.

Setting color-scheme per appearance also makes form controls, scrollbars and the canvas follow along.

No JavaScript ships with the extension - the attribute is there for whoever wants to build a switch.

Palettes 

Four alternate palettes ship alongside the neutral default. A palette varies accents only - primary , secondary , their hover states and the focus ring - while neutrals, semantic colour, spacing, radius and typography stay shared:

<html data-palette="ocean">
Copied!

Available: ember, ocean, moss, violet. Omitting the attribute selects the neutral default.

For a theme whose purpose is extension development, the palettes are a test surface rather than decoration: an extension that renders correctly across every palette in both appearances is one that is not hardcoding colour.

Re-theming without a build 

Custom properties survive compilation, so a site package can re-theme the extension from its own CSS without rebuilding the SCSS:

:root {
    --theme-color-primary: light-dark(#7b1fa2, #ce93d8);
    --theme-font-family-sans: 'Mulish', system-ui, sans-serif;
}
Copied!

Impact 

The palette is deliberately neutral. This is a theme for extension development, and its job is to make document structure legible without biasing the design of the extension being built against it.

Every colour was checked for contrast against both the background and the surface of its own appearance: body text clears 4.5:1 and any border that delimits a control clears 3:1. Semantic colour carries three tokens per meaning - the accent for text and borders, --theme-color-on-* for a foreground on a solid fill, and --theme-color-*-surface for the soft tint an alert sits on - because one value cannot do all three jobs.

The design is flat. There are no elevation tokens, and the only shadow is the focus ring, because a visible focus indicator is a requirement rather than decoration. It is split into --theme-focus-ring-color and a composite built around it, because light-dark() takes colours and not shadows.

Feature: Display settings behind a cog button 

Description 

The appearance and palette button groups of the site header are replaced by a single settings button with a cog at the end of the header. It opens a panel holding:

  • Appearance - Auto, Light and Dark, as one segmented control.
  • Palette - the five palettes as a list, each shown with its primary and its secondary colour.
  • Element outlines - a switch for the outline and the CType label around every content element, which could only be set with the constant theme.appearance.contentOutline so far.
  • Reset - back to the defaults of the site.

The two button groups took so much of the header that the site title and the main navigation had almost no room left. With one button, the brand, the main navigation and the settings share a single row at every width; below the breakpoint of the main navigation the row holds the brand, the menu toggle and the cog, and the expanded menu drops down under the header.

A choice applies at once and is kept in localStorage under three keys: theme-appearance, theme-palette and, new, theme-content-outline. The inline script in the document head applies a stored outline before first paint, like the other two. No cookie is set.

The panel is a disclosure holding native radio groups and a switch, not an ARIA menu: the radios keep their own keyboard behaviour, Escape closes the panel and returns focus to the cog, and Tab past the last control or a click outside closes it. Under forced colours the checked segment and the switch paint with system colours, so they stay distinguishable. The panel id and the radio names derive from the optional partial argument idPrefix (theme-settings by default), so a page can carry a second instance.

The defaults of the site 

The three constants of Feature: Appearance switcher stay what configures the defaults:

theme.appearance.default = auto
theme.appearance.palette = neutral
theme.appearance.contentOutline = on
Copied!

They now also reach the page template, as the FLUIDTEMPLATE settings settings.appearance.default, settings.appearance.palette and settings.appearance.contentOutline. The settings control checks its initial options from them and renders them as data-theme-default-appearance, data-theme-default-palette and data-theme-default-content-outline, which is where Reset takes them from. Reset removes the three stored keys rather than storing the defaults, so a visitor who reset follows the constants of the site again, including a constant changed afterwards.

A stored "Auto" is now kept as auto instead of removing the key, so on a site whose default appearance is not auto the choice is no longer lost on the next page.

Impact 

Resources/Private/Partials/Page/Settings.html replaces Resources/Private/Partials/Page/AppearanceSwitcher.html, and Resources/Private/Scss/components/_settings.scss replaces components/_appearance-switcher.scss. The classes .theme-appearance-switcher and .theme-appearance* are gone; the new ones are .theme-settings , .theme-segmented , .theme-swatch-list , .theme-swatch-option and .theme-swatch . A site package overriding Partials/Page/Header.html renders the partial Page/Settings instead of Page/AppearanceSwitcher.

The switch of the panel is a general form control, .theme-switch in forms/_controls.scss: a checkbox with role="switch", for a setting that takes effect the moment it is flipped.

<label class="theme-switch">
    <input type="checkbox" role="switch" checked> <span>Element outlines</span>
</label>
Copied!

The language labels theme.settingsLabel, theme.settingsContentOutline and theme.settingsReset are added; the appearance and palette labels are used as before.

Feature: Extbase plugin rendering 

Description 

A third-party Extbase plugin now renders on an installation using this theme, whether it is registered as a dedicated CType (the way TYPO3CMSExtbaseUtilityExtensionUtility::configurePlugin() recommends, and the only registration TYPO3 v14 accepts) or, on TYPO3 v13.4 only, through the historical General Plugin / list registration.

configurePlugin() generates tt_content.<pluginSignature> =< lib.contentElement for every plugin, unconditionally, on both installed core versions - even on v14, where nothing outside this theme defines lib.contentElement at all, because fluid_styled_content is not installed there. Before this change nothing rendered that object's templateName = Generic, so every such plugin fell through to TYPO3's own "no rendering definition" notice, indistinguishable to an editor from a broken content element.

Resources/Private/Templates/Generic.html is the new template that fixes that - shared by every plugin regardless of extension, because templateName = Generic is a fixed string the core writes itself, not something a plugin author controls. It reads back the per-plugin 20 cObject the core places beside templateName at a path built from the record being rendered (tt_content.{data.CType}.20), rather than a fixed one, so the one template serves every plugin without knowing which one it is. The plugin is rendered with its content element, so the settings an editor chose in its FlexForm reach it.

General Plugin / list 

TYPO3 v13.4 still offers the historical registration TCA (types.list in EXT:frontend's own Configuration/TCA/tt_content.php), deprecated but present (Deprecation forge#105076). fluid_styled_content supplied the tt_content.list object that rendered it, as a CASE keyed on the plugin's Type (list_type) field, and removed it outright in v14.0 together with the list CType itself (Breaking forge#105377). This theme now supplies that object too, in its own house style, reusing the same Generic.html template - a list record's own CType is list, so the same {data.CType}.20 path resolves to the CASE rather than to a single plugin.

It is declared unconditionally rather than behind a version condition: on v14 the list_type database column itself was dropped along with the CType, so nothing can ever reach the branch - verified directly against the installed v14.3.6 core, not only argued from the changelog. See docs/architecture/content-elements.md in the developer documentation for the full verification and the reasoning for leaving it unconditional.

Both delivery paths 

The rendering the core generates for a plugin is added to a site using site sets unconditionally, but to a site configured through a sys_template record only right after a static include that is registered as a content rendering template - the role fluid_styled_content plays for the installations that use it. The theme's static include is registered as one, so a plugin renders there as well. Before, a site using the static include rendered every other content element and showed the "no rendering definition" notice for plugins alone.

Impact 

An Extbase plugin registered by a third-party extension - one this theme does not control the TypoScript of - now renders instead of the core's "no rendering definition" notice, on both TYPO3 v13.4 and v14.3, regardless of whether it is registered as its own CType or, on v13.4, through the historical General Plugin type.

Tests/Functional/Fixtures/Extensions/plugin-fixture is a fixture extension that registers a plugin with no TypoScript rendering definition of its own - unlike Tests/Functional/Fixtures/Extensions/ example-fixture, which deliberately overrides what configurePlugin() generates - so the only thing that can make it render is this theme's own lib.contentElement and Generic.html.

Feature: More components in the library 

Description 

The component library gains the components a theme for extension development is most often asked to show, all of them pure HTML and CSS first, flat and framed like the rest of the library:

  • Tabs, dialog and tooltip - the three components that need the theme's script. Each of them stays usable without it: tabs fall back to every panel shown under its own heading - also when JavaScript is on but the script fails to load - a dialog opener is not shown, and a tooltip still appears on hover and focus.
  • Panel, for grouped content with a header, a body and a footer, and the close button.
  • Button: a link variant, an icon-only variant, a pressed and a busy state, and an attached button group for one control in several parts.
  • Forms: the input group - a control joined to text or a button - and the choice group for a set of checkboxes or radio buttons.
  • Alert: two asides next to the four severities, --note and --tip . The tip follows the selected colour palette.
  • Typography: heading levels four to six have a step of their own, and three text roles - display, lead and eyebrow - are classes.

A state these components draw in colour - a selected tab, a pressed toggle - is drawn in the system highlight colour under forced colours, and the components that depend on a fill for their edge get a border there instead.

What each one expects, and which of them need JavaScript, is on Components. The styleguide renders all of them, the three that need the script in a section of their own - see Feature: A styleguide page.

Impact 

Headings of level four to six render differently wherever they occur, content from the rich text editor included: they used to hold at the size of level three, and now step down to the body size and below it, with levels five and six set in capitals. The titles of the hero and the teaser, which follow the heading level an editor picks, look as before on every level.

Everything else is new and changes nothing that renders today. Resources/Public/JavaScript/theme.js binds the new components in addition to what it did before, and still has no dependency. The compiled stylesheet grows from roughly 63 kB to roughly 78 kB, uncompressed and before transport compression, and one design token is added, --theme-letter-spacing-caps , for text set in capitals.

Feature: External media, without loading it in advance 

Description 

A new content element, External media, in the Theme group of the Create new content element wizard. It shows a video of another site - and loads nothing from that site until a visitor presses play.

The editor gives it an address, a name, a shape and a poster image:

Field Is
Address of the video The page of the video, as you copied it out of the browser.
Name of the video Announced to a screen reader in place of the frame the video is loaded into. The heading of the element is used where it is empty.
Shape Widescreen (16:9) or Classic (4:3).
Poster The picture shown in place of the video until it is played. A file of this installation - a thumbnail fetched from the provider would be the very request this element avoids.

Until the button is pressed the page carries the poster, a note and a link to the source, and no iframe, no preconnect and no image of the other site. Nothing of the video's host - not a cookie, not an entry in its logs - reaches a visitor who does not watch it.

What is embedded, and what is not 

YouTube and Vimeo addresses are recognised in the forms a person actually copies - the watch page, the short link, the share link, the embed link - and are rewritten onto the host that sets no cookie in advance: youtube-nocookie.com, and player.vimeo.com with Vimeo's own dnt=1. An editor pasting the ordinary watch address does not have to know that.

Any other address is not embedded. The element then shows the poster and the link to the source, with no play button - the same page a visitor without JavaScript gets. That is deliberate and not a gap to be filled by pasting the address into an iframe: the field is not a trust boundary, an iframe runs the other origin's script inside the page, and a watch page of an unknown site is a page rather than a player - most sites refuse to be framed at all, so the reader would get an empty box and no explanation.

Without JavaScript 

The play button is not rendered at all until the theme's script has bound it. The link to the source is always there, so the video is one press away whether there is a script or not.

Impact 

Integrators get one new content type to grant to editor groups, theme_external_media. The database analyzer adds three columns to tt_content: tx_theme_embed_url, tx_theme_embed_title and tx_theme_embed_ratio. The element reuses the existing image column for its poster.

A site package overriding Templates/ContentElements/ finds the template there as ThemeExternalMedia.html, and the stylesheet gains the .theme-embed component - see Components. Which addresses become an embed is decided in SBUERKThemeExtensionDevelopmentDataProcessingExternalMediaProcessor; a site package with a fourth provider overrides that class, where the whole of that knowledge is.

The showcase page /elements/theme/external-media shows both shapes, an element without a poster and one whose host is not embedded.

Feature: File type icons in the file list 

Description 

The File Links content element renders its files as the file list component of the theme, and each of its three display types looks different:

Display file/icon/thumbnail Renders
Only file name The names of the files, one per row.
File name and file extension icon The icon of the file type before each name: PDF, image, archive, plain text, CSV, text document, spreadsheet, presentation, audio, video, and a plain file for any other type. The icon goes by the extension of the file.
File name and thumbnail (if possible) A square thumbnail of every image, and the icon of the file type in a square of the same size for every file that is not an image.

Show file size and Show description add the size and the description of the file reference in every display type.

Before, the icon of the second display type was not drawn at all, and it rendered like the first.

Impact 

The element renders <ul class="theme-file-list">, with the modifier --icon or --preview of the display type. The classes theme-content-element__file-list , __file-item , __file-link , __file-name , __file-thumbnail , __file-size and __file-description are no longer written; a site package that styled them styles the classes of the component instead.

The icon, and the thumbnail beside it, sit next to the link rather than inside it: the link is the file name. A thumbnail has an empty alternative text, because the name next to it already says which file it is.

The Layout field stays hidden for this element: the display type already is its layout.

Feature: A bridge to fluid_styled_content 

Description 

The theme renders every classic content element itself and does not require fluid_styled_content. An installation that has both can now say so, with a site set of its own:

dependencies:
  - sbuerk/theme-extension-development-fsc
Copied!

That set replaces sbuerk/theme-extension-development in the site configuration - it depends on it, so it brings the whole theme with it - and adds typo3/fluid-styled-content as an optional dependency, which activates that extension's own set for the site when it is installed.

For an installation without site sets there is a matching static template, Theme Extension Development (fluid_styled_content). It has to be included last, after both Fluid Content Elements and Theme Extension Development.

Why it is needed 

Side by side without the bridge, the two extensions are order dependent, and neither order is what an integrator chose:

  • Loaded after the theme, fluid_styled_content starts its own rendering with lib.contentElement > and rebuilds the object from nothing, taking the theme's Fluid paths with it.
  • Loaded before the theme, its per-element data processing stays underneath the theme's branches, because =< and a plain assignment keep every key the theme does not overwrite.

The second failure is the one to know about: both extensions wire Content elements for selected categories through DatabaseQueryProcessor and configure it differently, so the surviving keys compose one database query out of two and the page fails with a SQL syntax error.

The bridge clears the classic tt_content branches and declares them again from the theme's own definition, last. The rendered page is then identical to the page the theme renders on its own, whichever extension was loaded first.

Impact 

Nothing changes for an installation that does not have fluid_styled_content, and nothing changes for one that has it but does not enable the bridge.

One detail changes for everyone: the theme's Fluid root paths on lib.contentElement moved from index 10 to 5. Fluid tries root paths from the highest index down, and fluid_styled_content puts its own templates at 0 and the integrator's styles.templates.* override at 10 — identical in that extension's 13.4 and 14.3 releases, read in both. At 5 the theme's templates beat that extension's while a documented override still beats both.

An installation that overrides the paths through the theme's own constants - theme.templateRootPath and its two siblings - is unaffected, because those constants fill whichever index the theme uses. An installation that assigned lib.contentElement.templateRootPaths.10 directly still wins, and now keeps the theme's own path as a fallback below it, so a partial override no longer has to provide every template.

Feature: Font Awesome Free solid icons 

Description 

The theme ships the complete solid style of Font Awesome Free 7.3.1 - 2001 icons - and renders an icon inline as SVG with the new ViewHelper <theme:icon>:

<html xmlns:theme="http://typo3.org/ns/SBUERK/ThemeExtensionDevelopment/ViewHelpers"
      data-namespace-typo3-fluid="true">

<theme:icon name="circle-info" />
<theme:icon name="gear" class="theme-settings__icon" />
<theme:icon name="circle-info" label="Information" />
Copied!

The name is the file name below Resources/Public/Icons/FontAwesome/Solid/ without .svg. Without label the icon is decoration and carries aria-hidden="true"; with it, it is role="img" named by the label. The new component .theme-icon makes it one em square and fills it with the text colour. An unknown name throws an exception.

No webfont, no CDN and no request is involved. The icons are licensed under CC BY 4.0; LICENSE.txt and ATTRIBUTION.txt ship next to them, and every rendered icon keeps the attribution comment of its file.

The glyphs the theme drew by hand are now icons of the set:

  • the cog and the three appearance options of the display settings, and the check mark of the chosen palette,
  • the icon of each alert kind - in the notice content element and in the messages of the login form,
  • the cross of the close button,
  • the chevron of the accordion,
  • the error and success messages of form fields,
  • and the toggle of the main navigation, which shows the bars icon before its label.

What the stylesheet still draws itself is component geometry, not icons: the arrow of the tooltip, the spinner of a busy button and the thumb of the switch.

The styleguide has a new section, Icons, listing every icon the theme uses by name.

Impact 

The markup of three components changes, which matters to a site package that writes it itself rather than rendering the partials of the theme:

  • .theme-close no longer draws a cross. The icon is part of the markup:

    <button class="theme-close" type="button" aria-label="Close"><theme:icon name="xmark" /></button>
    Copied!

    A close button without it is empty.

  • .theme-field__error and .theme-field__success no longer put a character in front of the message. The message starts with the icon:

    <p class="theme-field__error" id="f-mail-error"><theme:icon name="circle-exclamation" /></p>
    Copied!
  • .theme-accordion__summary no longer draws the chevron. The icon is the last thing in the summary:

    <summary class="theme-accordion__summary"><theme:icon name="chevron-down" class="theme-accordion__marker" /></summary>
    Copied!
  • The glyphs in Partials/ContentElement/AlertIcon.html and Partials/Page/Settings.html are icons of the set, in the .theme-icon component. The check mark of a palette option is the check icon in every option, shown on the checked one.

Feature: Form showcase 

Description 

A new backend layout, Form showcase, renders a complete request form built from the form components of the theme - fieldsets, every common control, input groups, choice groups, a switch - followed by the same form as it comes back after a failed submission, with an error summary linking to each invalid field, and by the summary of a successful one.

The seeded demo tree carries it as /forms, next to /styleguide and, like it, hidden from the navigation. The start page of the tree links to both.

Like the styleguide, the page ignores content an editor places on it: the layout offers one column, and nothing renders it.

The forms send nothing 

Pressing a submit button runs the browser's own validation, so a field that is required or wrongly formatted is marked the way a real form marks it. The submission then ends in the browser: the forms use the dialog method outside of a dialog, which sends no request and leaves the page where it is.

Impact 

A site package can select the layout for a page of its own to look at its form styling in the appearance and palette it uses.

Feature: Enlarge on click opens a lightbox 

Description 

Enlarge on click - the field image_zoom of the Images, Text & Images and Text & Media elements - now opens the image in a dialog instead of only linking to the file.

Every image of the element that carries it gets a badge in its corner, and the dialog shows the file at its own size with the caption the file reference carries. Where the element has more than one image, the dialog moves between them: two buttons, and the arrow keys. Escape closes it, and focus goes back to the image that was pressed.

Nothing has to be configured. An element that already had Enlarge on click ticked shows the lightbox after the update.

Without JavaScript 

The link is still the link. The image keeps its href to the file, so a visitor without JavaScript - or one whose browser never loaded the theme's script - gets exactly what this element did before: the file, in the browser. The dialog is an enhancement on top of a link that works on its own, which is why the link is deliberately not the data-theme-dialog-open opener the dialog component documents: that attribute is hidden while there is no script, and hiding this link would take the fallback away with the enhancement.

Impact 

Integrators get one new component in the stylesheet, .theme-lightbox, and one new partial, Partials/ContentElement/Lightbox.html, rendered by Partials/ContentElement/Gallery.html once per element. A site package that overrides the gallery partial and wants the lightbox has to render it, and one that does not want it renders neither the partial nor the two data attributes on the zoom link.

The dialog carries one item per image, addressed by the id of its file reference rather than by a position - a Text & Media element may hold a video between two images, and a numbering would have to agree with a list the template does not have.

The badge on an enlargeable image is the magnifying-glass-plus icon of the shipped set, and the two arrows chevron-left and chevron-right - see Icons.

Feature: Layouts and an eyebrow for the heroes 

Description 

The three hero content elements - Hero, Hero, small and Hero, text only - have two new fields above their header.

Eyebrow is a short label above the title, such as a category or a date.

Layout arranges the text and the image:

Layout Renders Offered on
Image beside the text, at the start The hero as before. The default. All three
Image beside the text, at the end The image at the end of the row on a wide screen. Hero; hero, small
Centred Title, text, links and image centred. All three
Screenshot Centred, with the image below the text, cut off by the bottom edge of the hero - for a screenshot of an application. Hero
Cropped The image at the end of the row, cut off by the end and the bottom edges of the hero. Hero

A layout that arranges an image renders like the default without one; Screenshot renders centred.

Impact 

Existing heroes keep their look: the default layout is the one they always had. The layouts are the modifiers --image-end , --centred , --screenshot and --bordered of .theme-hero , and the eyebrow is <p class="theme-hero__eyebrow"> before the title.

A site package can remove layouts per hero in page TSconfig, for example TCEFORM.tt_content.tx_theme_hero_layout.types.theme_hero.removeItems = bordered.

Feature: Icon content elements 

Description 

Content elements of the theme's own that show icons of the Font Awesome Free solid set the theme ships, in the Theme group of the Create new content element wizard:

Content element Shows
Text and icon A heading, rich text and a link beside one icon.
Features A group of features, each with an icon, a title, a text and a link.
Figures A row of figures, each with what it counts, an optional sentence and an optional icon.
Steps The numbered steps of a process, each with a title, a text and an optional icon.

Every icon field offers the curated list of about a hundred icons the other icon fields of the theme offer - see Feature: Editors pick an icon by name. The icon is decoration: the text beside it says what it shows, so a screen reader skips it.

Text and icon 

The Icon palette holds the icon and three choices of how it is drawn:

Field Values
Icon position At the start of the line (the default), At the end of the line, Above the text
Icon shape Plain (the default) - the icon in the accent colour; On a square and On a circle - the icon on a tile of the accent colour
Icon size Medium (the default), Large, Extra large

The start and the end are those of the line, so in a right-to-left language the icon of At the start of the line is on the right. The heading takes the alignment and the style of the header palette, the text is rich text, and the link has the style and the icon of every theme link. An element without an icon shows its text alone.

The seeded demo tree shows every value on /elements/theme/text-icon.

Features, figures and steps 

The three take their items from the inline list the other list based elements of the theme use, and each item has an Icon field there - offered only in these three elements, which render it.

Features has a Layout palette with two fields:

Field Values
Layout Columns, the icon above (the default), Hanging icons, Tiles, With an introduction beside - the heading and the text of the element beside the features instead of above them
Columns, at most Two, Three (the default), Four - fewer where a column would be too narrow, one on a phone

Four columns need the full width of the content: on a page whose backend layout sets a sub navigation beside the content, the features show three columns at most.

A feature has a title, a text, an icon and a link with its label and its icon; the title is required.

A figure of Figures has the Figure itself - a number or a short value such as 4.5:1 - What it counts, both required, a text and an icon. Screen readers read it as "what it counts, the figure"; the figure is shown above, larger.

A step of Steps has a title, which is required, a text and an icon. The steps are numbered in their order; a step with an icon shows the icon instead of its number.

The seeded demo tree shows every layout and column count on /elements/theme/features, and the other two on /elements/theme/stats and /elements/theme/steps.

Impact 

Integrators get four new content types to grant to editor groups: theme_text_icon, theme_features, theme_stats and theme_steps. The database analyzer adds four columns to tt_content - tx_theme_icon_position, tx_theme_icon_shape, tx_theme_icon_size and tx_theme_columns - and two to tx_theme_list_item: icon, the icon of an item, and subheader, a short second line, which the figures use for what they count. The core field Layout is offered on the features.

A site package overriding Templates/ContentElements/ finds the templates as ThemeTextIcon.html, ThemeFeatures.html, ThemeStats.html and ThemeSteps.html. They render on the four components the component library gains - .theme-media-object, .theme-feature, .theme-stat and .theme-steps - see Components.

Feature: Editors pick an icon by name 

Description 

A field that stores an icon offers the Font Awesome Free solid set the theme ships as a select list grouped by the categories of Font Awesome, with a grid of the icons under it to click on. The stored value is the name of the icon, which a template renders with <theme:icon>.

A column - of the theme or of another extension - declares it with one line:

'tx_myextension_icon' => [
    'label' => 'Icon',
    'config' => \SBUERK\ThemeExtensionDevelopment\Tca\IconItems::selectConfig(),
],
Copied!

The configuration holds the No icon item, the groups and the icon grid. The icons themselves are added when the form is built, by an itemsProcFunc, from a list the theme builds once and keeps in the core cache - so they are not part of the TCA every request loads.

Page TSconfig narrows a field to the icons it names, and removes the narrowing again:

# Three icons for one field. The leading comma keeps "No icon".
TCEFORM.tx_myextension_domain_model_thing.tx_myextension_icon.keepItems = ,envelope,phone,globe

# Every icon of the set.
TCEFORM.tx_myextension_domain_model_thing.tx_myextension_icon.keepItems >
Copied!

A list written without the empty entry drops the No icon item, and the form then stores the first icon of the list with every record.

Impact 

The categories come from metadata/categories.yml of the pinned Font Awesome package and ship as Resources/Public/Icons/FontAwesome/categories.yml, next to the icons. An icon listed in several categories is placed in the first one. The names Font Awesome keeps for renamed icons are not offered: each draws the same glyph as an icon that is.

The grid shows each icon as an image of its file, which is drawn in black in either backend colour scheme.

After the icon set is changed by hand, flush the caches: the list is kept under a fingerprint of the set and its categories.

SBUERKThemeExtensionDevelopmentTcaIconItems::addItems() is the itemsProcFunc of the field and is public in the service container.

Feature: Rendering of the Images element 

Description 

The content element Images (CType image) is rendered by the theme.

No TCA is added for it. The element is registered by EXT:frontend, in Configuration/TCA/Overrides/225-tt_content-content_type-image.php, on TYPO3 v13.4 and v14 alike - what fluid_styled_content contributes for it is the rendering, and that is what this theme now brings itself.

The backend fields of the element decide the layout, and all of them are honoured:

Field Effect
Images The file references that are rendered, in their sorted order.
Number of columns The number of images per row.
Position and alignment Rendered as the modifier classes gallery--above, gallery--below, gallery--intext, gallery--left, gallery--center and gallery--right.
Width / Height A fixed dimension all images are scaled to, with the row scaled down when it would exceed the gallery width.
Enable click-enlarge Wraps the image in a link to the original file. No lightbox: this extension ships no JavaScript.

The Alternative text, Title and Description of a file reference become the alt attribute, the title attribute and a <figcaption>.

Every image is rendered with a width and a height attribute, so the browser can reserve its box before the file has loaded.

Impact 

An installation using the theme no longer renders the TYPO3 "no rendering definition" notice for Images.

The width the gallery is computed for is a constant and should match the width the layout gives the content column:

theme.media {
    maxGalleryWidth = 1200
    maxGalleryWidthInText = 420
}
Copied!

The markup is generated from Resources/Private/Templates/ContentElements/Image.html and Resources/Private/Partials/ContentElement/Gallery.html, which the Fluid path constants redirect together with every other template - see Configuration.

Feature: Initial theme development package 

Description 

Initial release of sbuerk/theme-extension-development. The package is a TYPO3 frontend theme for development purposes: its job is to give a TYPO3 installation a reasonable frontend to look at and to render against, without building a site package for it first — for extension development, for DDEV based test instances and for acceptance tests.

What this release provides:

  • TYPO3 v13 and v14 support on PHP 8.2 up to 8.5, with core version aware implementations below Core13/ and Core14/.
  • Dependency injection wiring through Configuration/Services.php, with services configured by Symfony dependency injection attributes on the classes themselves.
  • Container based tooling through Build/Scripts/runTests.sh covering linting, coding guidelines, static analysis, unit and functional tests and documentation rendering.
  • GitHub Actions workflows running these gates for TYPO3 v13 and v14 on pull requests.
  • A functional test setup ready to build on: strict PHPUnit configuration, an example fixture extension loaded by its composer package name, site based tests issuing frontend sub-requests in several languages, and repository tests running in a built frontend environment.
  • Developer documentation below docs/, covering the architecture, the quality gates, both test suites and the release workflow.

The extension ships a SBUERKThemeExtensionDevelopmentDummy placeholder class and an SBUERKThemeExtensionDevelopmentExampleExampleInterface example service, both meant to be removed once the first real implementation is added.

Feature: Language menu 

Description 

A site with more than one language shows a language menu in the site header, inside a dropdown. A site with one language shows nothing at all.

The menu lists every language of the site configuration. The language being read is marked; a language the current page has no translation for is shown as plain text rather than as a link.

That last point is the whole design of this feature. Leaving the language out would tell a reader the site has fewer languages than it has. Linking it would promise a translation that does not exist and land the reader on a fallback page without a word about it. So the language is shown, and shown to be unavailable - announced as disabled to assistive technology.

Whether a language is available for a page is decided by TYPO3, not by the theme: it is available when the page has a translation in that language, or in a non-default fallback language that does have one. Setting fallbackType: fallback with fallbacks: 0 does not make a page count as translated - the default language is filtered out of the overlay chain - although it does change what the page itself renders.

The dropdown 

The menu sits behind a labelled button in the header that discloses a panel below it. The panel is a native popover, so the browser opens it, closes it and puts it in the top layer: Escape and a click outside dismiss it. Tab does not close it - light dismiss reacts to those two routes, not to focus leaving the panel.

The dropdown works without the theme's JavaScript and is rendered whether the script loaded or not. The one thing the script adds is aria-expanded on the button, mirrored from the panel's own toggle event, because the Popover API tells assistive technology nothing about the trigger. A page whose script failed to load therefore has a working dropdown whose button reports a stale state, rather than a dead button.

Impact 

The development instances gain a second language, German, in config/sites/demo/config.yaml and config/sites/demo-legacy/config.yaml. The showcase itself is seeded in English only - the seeding tool cannot express a translation - so the demo shows the unavailable state on every page, which is the truth about that tree.

Adding a language to a site needs no database record: the sys_language table was removed in TYPO3 v12, and a site language is the site configuration alone.

The menu is core LanguageMenuProcessor at page.10.dataProcessing.40, so a site package drops it with page.10.dataProcessing.40 > and keeps the other three navigations. Its markup is Partials/Navigation/Language.html, the dropdown around it Partials/Page/Dropdown.html.

Two new components ship: .theme-language-menu and the generic .theme-dropdown - see Components. The dropdown is built on the native popover attribute, which is what moved the theme's browser floor to Firefox 125 - see Important: The browser floor moves to Firefox 125.

Feature: Themed frontend login form 

Description 

The login and the logout form of EXT:felogin are drawn with the form components of the theme: labelled fields, a required marker, the theme's button, and the result of a login attempt as an alert - a failed login as a danger alert, a logout as a success alert.

The theme ships two templates, Login/Login.html and Login/Logout.html, and adds their path above felogin's own (plugin.tx_felogin_login.view.templateRootPaths.20). The password recovery templates of felogin are not overridden. Every field felogin sends is kept, so redirects, the permanent login and the request token work as before.

The form renders on both delivery paths, the site set and the static include - see Feature: Extbase plugin rendering for what the latter needed.

Impact 

The login form looks like the rest of the site's forms without configuration. The theme's path sits at key 20, above felogin's own at key 10 - which is also where felogin's templateRootPath constant and site setting put a path. Templates configured that way are therefore shadowed by the theme's Login/Login.html and Login/Logout.html; a site package with templates of its own for felogin adds its path with a key higher than 20.

Feature: Cards and thumbnails for page menus 

Description 

The two page menus Menu of selected pages and Menu of subpages of selected pages gain a Layout:

Layout Renders
List The list of links it rendered before, and the default.
Cards One card per page: its first page media, its title and its abstract. The whole card is the link.
Thumbnails One small card per page: its first page media and its title.

The image of a card is the first file of the page's Media field (Resources tab of the page properties), with the alternative text of that file reference; a page without media gets a card without an image.

The field is offered for these two menus only. Every other menu, and every other content type the field does nothing for, keeps it out of the form.

Impact 

No new column. The page media is fetched only for a menu in one of the two new layouts, so a menu in the list layout costs what it did before. A record of another menu type that still carries a layout value from an earlier rendering renders its list as before.

A site package overriding Templates/ContentElements/MenuPages.html or MenuSubpages.html finds the cards in the new partial Partials/ContentElement/MenuCards.html.

Feature: Menu content elements 

Description 

The theme now renders the eleven menu_* content elements EXT:frontend registers - the last of the classic content element set left uncovered by Feature: Core content elements:

Content element Rendered
Pages A link per page in Pages, or the site root's own subpages when none are selected.
Subpages A link per child of the selected pages, or the current page's own children.
Section index The same, two levels deep, rooted at the current page when Pages is empty.
Section index of subpages from selected pages Subpages, two levels deep.
Sitemap The whole site, seven levels down from the site root.
Sitemaps of selected pages The same, rooted at the selected pages instead.
Abstracts Subpages, each link followed by that page's own Abstract field.
Recently updated pages Pages beneath the selection (or the current page), sorted by last change, each link followed by that date.
Related pages Pages beneath the same entry point sharing a keyword with it.
Categorized pages Pages carrying the selected category.
Categorized content Content elements carrying the selected category, linked by heading and anchor.

No TCA of this extension's own is added for any of them, the same as every other element Feature: Core content elements covered: all eleven were already creatable in the backend before this change, on TYPO3 v13.4 and v14 alike, and only rendered TYPO3's own "no rendering definition" notice.

Nine on MenuProcessor, two on a category query 

Nine of the eleven are the core's MenuProcessor, unchanged between v13.4 and v14.3, configured with a different special per type - list, directory, updated or keywords - and, for two of them, one extra level. Abstracts and Recently updated pages needed no data processor beyond that: MenuProcessor already JSON-encodes the whole page row onto every menu item, so the abstract text and the last-changed timestamp were already there for the reading.

Recently updated pages orders pages changed in the same second by descending uid, so the list comes out the same on every database system. The order is set with alternativeSortingField; a site package that changes special.mode - to tstamp, for example - changes that property along with it (tstamp DESC, uid DESC).

Categorized pages and Categorized content select by category membership, which MenuProcessor cannot express at all, and are built on two different mechanisms on purpose, not by accident:

  • Categorized pages uses the core's RECORDS cObject, which can select by category directly.
  • Categorized content uses TYPO3CMSFrontendDataProcessingDatabaseQueryProcessor with a subquery against sys_category_record_mm instead of RECORDS, even though RECORDS could select the same rows. Rendering through RECORDS would render every matched content element in full, nested inside this one - the wrong shape for a menu, and exposed to the same reference-cycle risk documented for Insert Records in Feature: Core content elements, which TYPO3 v14 does not guard at all. Rendering the matched rows as links instead means nothing can nest, so the cycle cannot form in the first place - no structural break was needed here the way one was added for Insert Records.

Markup 

A single new component, .theme-content-menu, is shared by all eleven elements - a list of links, optionally carrying a date or an abstract line, nested one level for Sitemap's tree. It is deliberately not the existing sub-navigation component: a menu_* element is authored content in the content column, not section-scoped site chrome, and reusing the navigation component would pull navigation styling into content rendering. See docs/development/component-library.md for the markup contract.

Known gap 

Historical fluid_styled_content additionally embedded each listed page's own content elements flagged "section index" into Section index and Section index of subpages from selected pages, linked by anchor. That is not implemented here. A second menu level - the listed pages' own children - stands in for it instead. A site package that needs anchor-level section navigation has to add it itself.

Impact 

An installation using the theme no longer renders the TYPO3 "no rendering definition" notice for any menu_* element. Tests/Functional/CoreContentElementRenderingTest.php sweeps all eleven together with every other covered CType and fails if any of them regresses to the notice; two further assertions render past the wrapper to confirm a menu actually lists what it should, and that the two categorized elements actually select by category rather than rendering a correct but empty wrapper.

The markup is generated from the templates below Resources/Private/Templates/ContentElements/, redirected together with every other template through the Fluid path constants under theme. - see Configuration. See docs/architecture/content-elements.md in the developer documentation for the full special/level table, the category-query reasoning, and the known gap above.

Feature: Navigation 

Description 

The theme now renders its three navigations: a main menu, a left-hand sub navigation for the current section, and a breadcrumb trail. All three are the same core MenuProcessor, configured three different ways, in Configuration/TypoScript/Navigation.typoscript.

Navigation Source Rendered on
Main menu Site root, two levels every page, in the header
Sub navigation The current page's section, two levels the Content page with sidebar layout only
Breadcrumb The rootline, root to current page Content page and Content page with sidebar

The sub navigation fixes a rootline position, not the current page 

The sub navigation is configured against the current page's first-level ancestor, not the current page itself, using special.value.data = leveluid:1 rather than entryLevel.

The difference matters because it is easy to get backwards, and the wrong choice looks correct in the one place it is checked first. entryLevel is relative to the current page's depth - the same value resolves to a different ancestor on a second-level page than on a third-level page. leveluid:1 instead indexes the current page's rootline directly: index 0 is always the site root and index 1 is always the first page below it, on every page regardless of how deep it is.

That is what keeps the sidebar showing the same section three levels down as it does on the section's own landing page, rather than emptying out underneath it. A sub navigation built from the current page's own children looks entirely correct on a first-level page - there, the section root is the current page - and only fails on the pages below it, exactly where a reader needs the navigation most. Tests/Functional/NavigationRenderingTest.php therefore runs against a fixture three levels deep and asserts the same section content at all three, rather than stopping at the depth where the bug would already be invisible.

Placement follows the backend layout 

Each navigation is its own numbered TypoScript key (page.10.dataProcessing.10/.20/.30), so a site package can remove exactly one without touching the others:

page.10.dataProcessing.20 >
Copied!

The sub navigation is rendered only on the Content page with sidebar layout - the one layout with a left column, per Backend layouts decide the page template. Choosing that layout in the page module is how an editor asks for the left navigation; there is no separate flag. The breadcrumb renders on Content page and Content page with sidebar, and deliberately not on Start page (no trail is worth showing there) or Default (the bare layout, which is also the one layout that already renders the page title itself).

Accessibility 

  • Every <nav> carries a translated aria-label - three navigation landmarks on one page are indistinguishable to a screen reader without one.
  • The current page carries aria-current="page", and the stylesheet styles that attribute directly rather than a modifier class kept in sync with it - the visual state and the announced state cannot disagree.
  • The breadcrumb's last item is plain text with aria-current="page" on the list item, not a link: the destination of a breadcrumb is not somewhere it still points to.
  • The breadcrumb separator is generated content on a ::before , never a character in the markup, so it is not part of the accessible name and is not announced.

Impact 

The main menu's toggle button ships without its script. The markup - the <button aria-expanded="false" aria-controls="nav-main"> and the matching id on the list - is in place, but the script that flips aria-expanded and the data-js marker the stylesheet gates collapsing behind both arrive with the script of Feature: Appearance switcher. Until then the button is inert and hidden by CSS, and the menu is simply always expanded - the intended, working state without JavaScript, not a degraded one. See Feature: Component library for the data-js switch itself.

Feature: Notice, tabs and accordion elements 

Description 

Three more content elements of the theme's own, in the Theme group of the Create new content element wizard. Each puts a component of the library into an editor's hands that had no content element so far:

Content element Renders through
Notice The alert component, in one of six kinds.
Tabs The tabs component: one tab per item, one panel at a time.
Accordion The accordion component: collapsible items, one open at a time.

Notice 

The Kind field offers the six kinds of the alert component: Note, Information, Tip, Success, Warning and Danger. The kind also decides how a screen reader treats the notice, and an editor does not choose that separately:

Kind role
Information, Success status - read when the reader pauses
Warning, Danger alert - read at once
Note, Tip note - an aside, never announced

A new notice is a Note: of the six, it is the kind that never interrupts anybody. The title is shown inside the box, and the text is rich text.

Tabs and accordion 

Both take their items from the same inline list the other list based elements of the theme use - a title and a text per item - and the text is rich text for these two. Tabs shows every panel under its heading until the theme's script has run, so a page without JavaScript loses nothing. Accordion needs no script at all: its items share a name, and the browser keeps one of them open.

Impact 

Integrators get three new content types to grant to editor groups: theme_notice, theme_tabs and theme_accordion. One new column, tt_content.tx_theme_notice_kind, is added by the database analyzer.

A site package overriding Templates/ContentElements/ finds the three templates there as ThemeNotice.html, ThemeTabs.html and ThemeAccordion.html.

Feature: Pricing plans 

Description 

A content element of the theme's own, Pricing, in the Theme group of the Create new content element wizard. It renders pricing plans side by side, and takes its plans from the inline list the other list based elements of the theme use.

A plan has a name, a price, what the price is per, its features and a link:

Field Is
Title The name of the plan. Required - it is what a reader refers to.
Price Required, and written out as the site writes prices: the element formats nothing and adds no currency of its own.
Per What the price is for - "per month", "per seat and month".
Features One feature per line.
Highlighted Singles this plan out from the others. Meant for one plan of a group.
Link With a label, an icon and a Link style - the same four styles the link of a hero or a card offers - rendered as a button.

The plans share one row from 48rem up, in columns of equal width, and stack below it. There is no column count to pick: a pricing table is read across, and a fourth plan wrapping onto a row of its own is the one arrangement that stops a reader comparing them. Every button sits on the same line whatever the feature lists above them did.

The prices are set in tabular figures, so the digits line up between the columns - "19 €" and "99 €" in two plans start and end at the same place.

A feature a plan does not include is left out 

The feature list says what a plan includes. A feature it does not include is left out of its list rather than shown crossed out, and the plan beside it is the comparison.

Showing exclusions would need a syntax inside the field saying which line is which - a leading -, say. That is a convention no editor is told about, and a plan whose author did not know it would silently render its features as exclusions. The theme made the same choice for the definition list of the bullet element, and for the same reason.

Impact 

Integrators get one new content type to grant to editor groups: theme_pricing. The database analyzer adds one column to tx_theme_list_item, highlighted; the price is the existing subheader and the period the existing meta, relabelled for this relation rather than given columns only this element could use.

A site package overriding Templates/ContentElements/ finds the template there as ThemePricing.html. The stylesheet gains the .theme-pricing component - see Components - whose feature list is the existing .theme-list--check , so the check mark is the one icon of the shipped set that component already draws.

The showcase seeds the element on /elements/theme/pricing, and the /styleguide page shows the component in its own Pricing section.

Feature: Pull quote and centred testimonial style 

Description 

The Testimonial content element has a new field Style:

Style Renders
A rule at the start of the quotation The testimonial as before. The default.
Pull quote The quotation larger and bolder, between a rule above and a rule below, with a quotation mark.
Centred The quotation and its attribution centred below a quotation mark, no wider than the reading measure.

The quotation mark is an icon of the theme's icon set, hidden from screen readers - the quotation itself is marked up as one.

Impact 

Existing testimonials keep their look. The styles are the modifiers --pull and --centred of .theme-quote , and the quotation mark is <span class="theme-quote__mark" aria-hidden="true"> before the <blockquote>.

Feature: Rich text preset 

Description 

The theme ships a preset for the rich text editor, theme, and selects it for every rich text field. Its styles and alignments write the classes the theme styles, instead of the Bootstrap classes of the core preset, which the theme does not style:

Offered as Writes
Lead, Eyebrow A paragraph with class="theme-lead" or class="theme-eyebrow"
Small print, Marked, Keyboard input, Code <small>, <mark>, <kbd>, <code>
Alignment left, centre, right, justify theme-text--start, theme-text--center, theme-text--end, theme-text--justify

Right and left follow the direction of the text. Justified text is hyphenated in the language of the page.

The preset needs the system extension rte_ckeditor, which the theme suggests and does not require. Without it the preset is not registered and rich text fields behave as before.

Impact 

The preset is selected by page TSconfig for the whole installation:

RTE.default.preset = theme
Copied!

The line comes from Configuration/page.tsconfig of the extension, which TYPO3 loads for every page tree of the installation, the same way the theme's backend layouts are registered. So it also reaches a site that does not use the theme. That site selects its own preset in its own page TSconfig, which the core loads after that of the extensions. The TSconfig can come from the site's set, from config/sites/<site>/page.tsconfig, or from the Page TSconfig field of its root page:

RTE.default.preset = default
Copied!

A preset selected for a single field or type still takes precedence. To add styles, a site package imports EXT:theme_extension_development/Configuration/RTE/Theme.yaml into a preset of its own and registers that one - see the developer documentation of the extension.

Content saved with the core preset keeps its Bootstrap classes, which the theme does not style.

Feature: A seeded showcase of every element 

Description 

The extension ships a showcase page tree as a seed set of sbuerk/data-factory, in EXT:theme_extension_development/Configuration/DataFactory/theme-demo/. That extension is suggested, not required: the set is inert data until it is installed, and nothing in the theme reads it.

composer require --dev sbuerk/data-factory
vendor/bin/typo3 data-factory:import theme-demo
Copied!

The tree demonstrates the whole theme rather than a handful of pages:

Page Backend layout What it shows
Theme demo (/) Start page The site root, and the footer columns that layout adds.
Typography (/typography) Content page Headings, running text and the inline cases a stylesheet has to answer for.
Media (/media) Content page A single image, and a two column gallery.
Empty page (/empty) none A page with no layout selected, which falls back to the default.
Elements (/elements) Content page The showcase branch, and the parent of the three pages below.
Core elements (/elements/core) Content page with sidebar Every classic content element the theme renders, once each.
Menu elements (/elements/menu) Content page with sidebar All eleven menu elements, each pointed at a different part of the tree so they are told apart by what they list.
Theme elements (/elements/theme) Content page Every element the extension registers itself, with their inline children filled in.
Styleguide (/styleguide) Styleguide The component library, rendered from Fluid rather than from content.
Forms (/forms) Form showcase A request form built from the form components, see Feature: Form showcase.

Between them the pages use every backend layout the extension registers and carry every content type it renders, so a single seeded instance answers what the theme does with each. Two of the pages are deliberate special cases: Empty page selects no backend layout at all, which is the only way to see the default fallback. Styleguide and Forms are in the main navigation, see Feature: A showcase page per element and per variant; neither is disabled - a disabled page returns 404 in the frontend and is only reachable through a preview link, which defeats the point of seeding a page that exists to be opened.

Known limitation 

Categorized pages and Categorized content are part of the seeded tree but select nothing: the set seeds no sys_category records. Both elements render an empty menu, which is the correct rendering of "no category chosen".

Impact 

vendor/bin/typo3 data-factory:import theme-demo produces a frontend that exercises the theme end to end, so a development or test instance needs no pages built by hand to see what an element looks like.

The set declares the uid of every record it writes - pages 1 to 10 and the pages of Feature: A showcase page per element and per variant, content elements from 101 - because the records point at each other by uid: the links name t3://page?uid=2, the Insert records element names tt_content_601, and a site configuration names its root page. The import therefore needs an installation where those uids are free, and refuses rather than overwrites when they are not. --root-page=<uid> writes the tree below an existing page instead of at the root of the page tree, which changes where it lands and not which uids it takes. The set declares no site configuration: create one with root page 1 after importing it at the page tree root.

Feature: A showcase page per element and per variant 

Description 

The showcase set theme-demo - see Feature: A seeded showcase of every element - grows from one page per family of content elements to one page per element:

Page What it shows
Core elements (/elements/core) and one page below it per classic content element, at /elements/core/<CType> Header in every level and alignment, Text & Images in each of its ten image positions, Text & Media, Images in one to four columns, Bullet List in every layout and list type, Table in every table class, File Links in every display type, Divider, Plain HTML and Insert Records.
Frames (/elements/frames) Every frame, every space before and after, and every header alignment and look of the Appearance tab, one element each.
Typography (/typography) and its pages Text, Lists, Tables, Quotes and code and Article The typography of the theme as an editor produces it, from rich text and content elements: headings inside a text, the inline semantics, the alignments and styles of the rich text preset, lists and tables of the rich text next to the elements, quotations, code blocks, and a long article that uses all of them.

Three sections are in the main navigation: Elements, Typography, and the component library on Styleguide with the form showcase on Forms, which used to be left out of every menu. The pages of a section are its sub pages, two levels deep in the main navigation and in the sub navigation of the section. Empty page, which exists to show the default layout, left the menus instead: it is reached by its URL and from the theme elements, and as a sixth top level entry it pushed the site title of the header onto a second line on a wide screen.

On a wide screen the section index of the styleguide stands beside the sections and stays in view while they scroll past; on a narrow one it is the list of links above them.

The rich text of the typography pages is written into the database the way a backend save writes it, so the processing of the installation's rich text preset applies to it. With the preset of the theme everything on the pages survives; it was not typed into the editor, and several parts need its source view.

Impact 

The set declares more uids: pages 30 to 39 and 50 to 56, and their content elements at the page uid times 100 plus the position, for example 3502. An installation importing theme-demo needs those uids free as well.

The new pages skip decades on purpose. The development instances mirror the showcase with every uid moved by 1000, which puts the mirror of the content of a page on the uids of the content of the page ten uids further; see the rule at the top of the scenario file.

Feature: Site set and page rendering 

Description 

The theme now renders pages, and ships a site set to enable it.

A site enables the theme by depending on the set in its site configuration:

dependencies:
  - sbuerk/theme-extension-development
Copied!

No sys_template record is needed. The set brings the TypoScript, a Fluid based page rendering and the compiled stylesheet with it.

For installations that do not use site sets, the theme additionally registers a classic static template, selectable in the Include static (from extensions) field of a sys_template record. The two mechanisms are safe side by side: the static include detects an active set and skips its own import, so a site configured with both is not served the theme twice.

What is rendered 

  • A page object with a Fluid template, layout and partials below Resources/Private/.
  • The compiled stylesheet from Resources/Public/Css/theme.css.
  • The content of the normal column.

The Fluid paths and the stylesheet are TypoScript constants under theme., so an integrator can point them at their own files without editing the extension — see Configuration.

Feature: A styleguide page 

Description 

The extension now ships a styleguide: one page that renders every component of the component library from its own Fluid templates, so the whole theme can be looked at in one place, in the appearance and colour palette currently selected.

The seeded demo tree carries it at /styleguide (Feature: A seeded showcase of every element), reachable by URL and hidden from every menu. It is set to Page not enabled in menus rather than disabled: a disabled page answers 404 in the frontend and needs a backend preview link, which defeats the point of a page that exists to be opened.

Eight sections, each its own partial:

Partial Section What it shows
Styleguide/Tokens.html Tokens All 27 colour tokens as swatches, plus the type scale, weight and family, the spacing scale, radius, shadow and the focus ring.
Styleguide/Typography.html Typography The element baseline every tag gets without a class - headings, running text, inline elements, lists, quotations, preformatted text, rules - and the table component.
Styleguide/Buttons.html Buttons .theme-button with every modifier, size and state it ships, the close button, the button group, and .theme-badge in both severities and fills.
Styleguide/Boxes.html Boxes Card, panel, teaser, hero, quote, alert, accordion and author, each with every modifier its stylesheet defines.
Styleguide/Interactive.html Interactive Tabs, dialog and tooltip - the components that need JavaScript - and what each of them does without it.
Styleguide/Forms.html Forms The complete form contract: the field wrapper, every input type, select and textarea, fieldsets, checkboxes and radios, the validation states, disabled and read-only.
Styleguide/Navigation.html Navigation Main and sub navigation, breadcrumb, pagination and the content menu.
Styleguide/Media.html Media The gallery in one, two and three columns, and the content element wrapper with its outline switch.

The page ignores its own content 

Nothing on this page comes from a content element, a record or a data processor. That is deliberate: a styleguide shows the contract, and a specimen assembled from a record would show whatever that record happened to contain and would break for reasons that have nothing to do with the component.

Two things implement it. The Styleguide backend layout offers a single column with colPos 999, which no TypoScript object in the extension reads - anything an editor places there is stored and never rendered, inert rather than broken. And the template contains no f:cObject at all, not even for the main column.

Overriding a section 

Each section is a partial of its own under EXT:theme_extension_development/Resources/Private/Partials/Styleguide/, so a site package that wants its own forms section overrides Styleguide/Forms.html and keeps the other seven. That is the same fine-grained override the rest of the theme follows.

Specimen copy is literal English and is not routed through locallang.xlf. Specimen text exists to be set in a typeface; it has nothing to localise, and roughly 150 translated labels would make the specimens unreadable in the source, which is the one place they have to be readable.

Impact 

Importing the showcase with vendor/bin/typo3 data-factory:import theme-demo gives a frontend where /styleguide answers with the whole component library. It is the only place the design tokens can be seen resolved rather than read as values, and because of that it doubles as a live test of the appearance and palette settings: change either one behind the cog in the header, and every swatch and every specimen on the page has to move together. One that does not is a colour that escaped the token layer.

The Elements pages of the demo tree remain complementary rather than redundant. Those render the same components from real content records, so they prove the wiring; this page proves the library. A component can break in one without the other noticing - most of the form contract, for instance, is reached by no content element at all.

Three fixes come with it 

All three were found by putting a second instance of a component on one page, which nothing before this did:

  • Only the first main navigation toggle was bound. The stylesheet collapses every .theme-nav-main whose toggle is not expanded, while the script wired up only the first toggle on the page. A second navigation - a footer menu repeating the main one - was collapsed below the breakpoint by a control that did nothing, with no way left to open it. Every toggle is now bound, scoped to the navigation it belongs to.
  • .theme-form-summary--error matched no rule. It was part of the published markup contract and of the documentation but had never been declared. The base rule already carries the danger palette, so it looked correct; it is now declared explicitly, symmetric with .theme-form-summary--success .
  • The alert markup contract showed the wrong role. It illustrated role="status" on a .theme-alert--warning , which invites copying an assertive severity as a polite one. --info and --success take role="status"; --warning and --danger take role="alert".

Feature: A portrait for the testimonial 

Description 

The Testimonial content element has an Images tab again, with one field, Portrait, for one image of the person quoted. The portrait is shown as a round avatar at the start of the attribution, before the name.

The portrait appears only when the element has a name: next to the name it is decoration, and a screen reader reads the name once. An element without a name shows no portrait.

Impact 

The attribution of a testimonial with a portrait starts with <span class="theme-avatar theme-avatar--large theme-quote__portrait">, holding the image with an empty alternative text. The alternative text of the file reference is not used there.

Feature: Text in columns 

Description 

The Layout field of the Text content element, in the Appearance tab, is offered again and has two values:

Layout Renders
Running text The text in one column, as before.
Columns The text in two columns where each can be at least 30 characters wide, and in one column on a phone. A hairline separates the columns; a heading stays with the text it opens, and a quotation, a table, a code block or a list item moves to the next column whole.

The layouts Layout 2 and Layout 3 of the core are not offered for this element. A text element that carries one of them renders as running text.

Impact 

A text element with Columns renders its rich text in <div class="theme-content-element__body theme-text--columns">. The class can be used on any block of rich text in a template of a site package.

A site package can relabel or remove the layouts in page TSconfig, below TCEFORM.tt_content.layout.types.text, and change the number and the width of the columns through the custom properties --theme-text-columns-count and --theme-text-columns-min-width .

Feature: Theme content elements 

Description 

The theme now ships ten content elements of its own - unlike every element covered in Feature: Core content elements and Feature: Menu content elements, these do not exist in the core at all. Their TCA, their own columns and a shared inline child table all belong to this extension:

Content element Renders through
Hero A full hero: heading, text, media and call-to-action links.
Hero, small The same, reduced.
Hero, text only The same, without media.
Teaser A short text teaser without media.
Media teaser Text placed beside a single image.
Media teaser grid Several media teasers arranged in a grid.
Testimonial A quotation with its attribution.
Author A person: portrait, name, role and links.
Link list A list of links.
Social links The same, labelled instead of rendered as icons.

All ten are grouped under their own Theme entry in the Create new content element wizard, so an editor can tell them apart from the core set at a glance. No page TSconfig registers that group - since TYPO3 v13 the wizard is generated straight from the label, description, group and icon already given to ExtensionManagementUtility::addRecordType().

Naming 

CTypes are prefixed theme_, columns tx_theme_, and the shared inline child table is tx_theme_list_item - short rather than the full extension key, because themeextensiondevelopment_hero is unusable in a showitem string and in TypoScript. This follows the reference implementation's own equally short prefix for the identical reason, and it accepts the same collision risk deliberately: another extension is free to also prefix its own fields theme_.

No schema of its own 

This extension ships no ext_tables.sql. The whole schema - the four tx_theme_* columns added to tt_content and every column of tx_theme_list_item - is derived from TCA by TYPO3CMSCoreDatabaseSchemaDefaultTcaSchema::enrich(), on both supported core versions.

One column needed to be declared explicitly rather than left to that derivation. An inline relation's foreign_field and foreign_table_field are auto-created on the child table if not already present; a field used only in foreign_match_fields - here, the column that records which of the four inline relations sharing this child table a row belongs to - is not part of that special case and gets no column for free. fieldname on tx_theme_list_item is therefore a real type=input column, the same way the core's own sys_file_reference declares its own fieldname for the identical reason.

Inline children, and the item.data trap 

No core data processor resolves a generic database relation the way FilesProcessor resolves FAL - that class only ever wraps FileCollector, which is FAL-specific by construction. Author, Link list, Social links and Media teaser grid all resolve their shared inline relation with TYPO3CMSFrontendDataProcessingDatabaseQueryProcessor instead.

That processor wraps every row as ['data' => $record] - the same wrapping Feature: Menu content elements already documents for Categorized content. Every template reads item.data.link, never item.link: the latter resolves to nothing and renders an empty list with no error, not a broken one. This is not a hypothetical risk here either - it happened during this element set's own development, before the functional test suite caught it.

GalleryProcessor is deliberately not used 

Hero, Hero, small, Media teaser and Author all resolve their image with FilesProcessor alone. Each shows exactly one image in a fixed-shape box, and none of their forms expose Columns, Orientation or the other gallery fields GalleryProcessor reads - wiring it in would bind the layout to columns an editor can never set.

Markup 

Link list and Social links reuse the existing .theme-content-menu component - the same one every menu_* element uses - rather than a list component of their own: structurally the shape is identical, and a purpose-built list component would only duplicate styling that already exists. Author's own profile/contact links reuse the identical pair for the same reason.

Only one new component was needed: .theme-author - a portrait, a role line and a bio. It does not render the person's own name; that goes through the shared content-element heading like every other element, so .theme-author sits below it rather than repeating it.

Known gaps 

This theme ships no icon assets and no icon component. Social links therefore renders the same text-label list as Link list - link_label stands in for a platform icon, not a glyph approximating one.

.theme-hero__eyebrow exists in the hero component's stylesheet, but no hero variant's TCA offers an eyebrow field to back it - omitted rather than invented.

A field was removed 

Testimonial originally exposed the core Images field, the same way Hero and Author do. The quote component it renders through has no media slot at all, so a filled-in image would never have appeared on the page - an editor attaches a portrait and the work is silently gone. The field was removed from the form rather than left inert.

Impact 

An installation using the theme can create all ten elements from its own wizard group and gets working output for every one of them. Tests/Functional/ThemeContentElementRenderingTest.php renders a page carrying one of each and asserts every element reaches the content-element wrapper, that the inline-relation based elements actually list their children (not merely an empty, correct-looking wrapper), that inline children keep the order an editor gave them, and that the button variant and the link field resolve to a real, followable URL rather than a raw t3:// reference.

See docs/architecture/content-elements.md in the developer documentation for the full CType table, the schema derivation details, and the reasoning behind each decision summarised above.

Feature: Typography and text components 

Description 

The typography of the theme covers what running text and rich text actually contain, and the /styleguide page shows all of it.

  • Display sizes: .theme-display gains three sizes, --1 , --2 and --3 . Two new design tokens carry the added ones, --theme-font-size-display-1 and --theme-font-size-display-3 .
  • Element typography, without a class: a secondary line in a heading, balanced headings and paragraphs without a lone last word, the defining instance of a term, key combinations, quotation marks per language, hyphenation of German text, long links that wrap, and captions and table cells that align to the start of the line in right-to-left text as well.
  • List, .theme-list : unstyled, inline, check marks, an icon per item, two or three columns, and divided rows.
  • Description list, .theme-dl : stacked, or terms and descriptions side by side with --horizontal , optionally with truncated terms.
  • Figure, .theme-figure : an image with a caption and a credit line, floated to either side of the running text.
  • Code block, .theme-code : a code block with its file name, which can be scrolled with the keyboard.
  • Divider, .theme-divider : a separator with a label, and a section break.
  • Text and image in text: the gallery of the text and image and the text and media elements now floats beside the text when its position is "In text", and keeps the text beside it for "In text, no wrap".
  • Table, .theme-table : striped rows or columns, a highlighted row under the pointer, column rules, no rules, compact cells, a header row that stays in view, a caption below the table, groups of rows and a totals row. The Table class of the table content element offers every one of them: the core's Striped and Bordered, and five more the theme adds through page TSconfig.

See Components.

Impact 

Paragraphs may wrap differently wherever they occur, rich text included: the browser now avoids a single word on the last line of a paragraph. German text is hyphenated where it used to overflow or leave a gap. Table captions and cells in a right-to-left page align to the right, as they should have before.

The gallery of a text and image or text and media element positioned "In text" used to sit above the text; it now floats beside it on a viewport of the medium breakpoint and wider. Every gallery item carries the additional class theme-figure, and its caption theme-figure__caption.

A table content element with the default table class is no longer striped - see Important: Tables are no longer striped by default. A table with a footer row shows it as a totals row.

Important: The browser floor moves to Firefox 125 

Description 

The oldest browsers this theme supports are now Firefox 125, Chrome and Edge 125, and Safari 17.5. The previous floor was Firefox 120.

It moved for the Popover API. The toggletip and the language dropdown of the site header are built on popover and popovertarget, which needs Firefox 125. Both components would otherwise need a hand-written script for showing, hiding, light dismiss and the top layer - behaviour the platform already has and gets right.

Nothing else about how the floor is chosen changed: it is the oldest version of each engine that supports every feature the theme actually relies on, never a feature it merely could use.

Impact 

On a browser older than the floor the two components built on popover do not open. Everything else renders: the theme uses no other feature above the old floor, and light-dark() , :has() , color-mix() and the logical properties it has always used are all below it.

If a site has to serve browsers older than this, the two affected components - the toggletip and the dropdown around the language menu - are the ones to replace or leave out. The language menu itself is a plain list of links and works anywhere; only the dropdown around it needs the newer browser.

Two features remain out of scope even though the floor moved, because both are still above it: CSS anchor positioning, which is what the tooltip would need to keep its bubble inside the viewport whatever its trigger, and invoker commands (command / commandfor), which is what the dialog would need to open without a script. Each is a change of its own.

Important: Button groups and tables keep a margin 

Description 

The button group, .theme-button-group , and the wrapper of a table, .theme-table-wrapper , now end on a bottom margin, the one every other block of the component library ends on - a list, a figure, a code block, a paragraph. Before, the component after them touched them: a list right under a row of buttons, a figure right under the border of a table.

Impact 

Whatever follows a row of buttons or a table moves down by var(--theme-space-4) , 20 pixels. That includes the table content element, which renders its table in the wrapper. A button group or a table at the end of a box - a panel, a band - leaves that margin inside the box, as a list or a paragraph already did.

A site package that wants the previous spacing sets the margin back in its own CSS, included after theme.css:

.theme-button-group,
.theme-table-wrapper {
    margin-block-end: 0;
}
Copied!

Important: Form controls draw a stronger border 

Description 

The text input, the textarea, the select and the addon of an input group now draw their resting border in --theme-color-border-strong instead of the decorative --theme-color-border . An empty text field is identified by its edge alone, and WCAG 1.4.11 requires 3:1 against the colours next to it; the decorative border reached about 1.4:1.

On hover the border of an input now turns to --theme-color-text-secondary , since the colour it used to turn to is now its resting colour. The invalid and valid states are unchanged.

The dark value of --theme-color-border-strong moves from #5f6c7d to #637183 . The old value reached only 2.89:1 on --theme-color-surface-raised , the background of a control; the new one reaches 3.10 there, and 3.74 and 3.44 on background and surface. The light value is unchanged.

Impact 

Form controls look more defined in both appearances, and every border drawn in --theme-color-border-strong - the switch track, the display settings panel, the dialog, table headers, kbd - is slightly lighter in dark.

A site package that preferred the previous look re-points the component tokens in its own CSS, without rebuilding the theme:

.theme-input,
.theme-textarea,
.theme-select {
    --theme-input-border-color: var(--theme-color-border);
    --theme-input-border-color-hover: var(--theme-color-border-strong);
}

.theme-input-group {
    --theme-input-group-addon-border-color: var(--theme-color-border);
}
Copied!

The selectors carry the same specificity as the theme's own rules, and the theme uses no cascade layers, so the site package's stylesheet has to be included after theme.css. The invalid and valid states still win, they are more specific.

Doing so gives up the 3:1 boundary of the controls again.

Important: A definition list reads "term|description" lines 

Description 

A Bullet List of the type Definition list used to render every line as a term of its own, with no description at all.

Each line is now split at the vertical bar, the way fluid_styled_content reads the same field: the part before the first | is the term, every further part a description of it.

Surface band|The element on the surface colour, with a hairline around it
Inverse band|The element in the other appearance
No frame
Copied!

renders three terms, the first two with a description. A line without a vertical bar is a term without a description. An empty line, a line with an empty term - |description - and an empty description are skipped: a description is not rendered without its term.

A line is read as a line of CSV with the double quote as the enclosure, so a part may be enclosed in double quotes to hold a vertical bar itself.

Impact 

A line that contains a vertical bar is split where it was not before. A line without one is still one term, with two exceptions that come from the CSV reading:

  • A line that starts with a double quote loses it, and the text up to the closing quote is read as the enclosed term.
  • A double quote at the start of a line that is never closed swallows the lines after it into that one term, up to the next double quote or the end of the field.

fluid_styled_content reads the field the same way, so it shows the same terms and descriptions for such lines, except that it renders an empty term and an empty description where this theme skips them.

Important: The main navigation wraps before the title 

Description 

On a wide screen the site header keeps the site title, the main navigation and the display settings in one row. From 1024 pixels up, when the row gets tight, the top level of the main navigation now wraps onto a second row inside its frame, and the site title keeps its line. It used to be the other way round: the navigation did not shrink, and a sixth top level entry pushed the site title onto a second line at 1280 pixels. The header now holds seven top level entries next to the title of the showcase at that width. The title takes at most half the row; a title longer than that still wraps, as the last resort.

Between 768 and 1024 pixels the row is too narrow for that trade, and the menu keeps its row as it did before, as long as it takes at most 60% of the row; the title wraps beside it. A menu wider than that wraps inside that width, so the header no longer spills sideways. Below 768 pixels nothing changes: the menu is behind its toggle.

Impact 

From 1024 pixels up, a site with more top level entries than fit beside its title gets a two row menu instead of a two line title. Between 768 and 1024 pixels a site whose menu fits in 60% of the row looks as before: the title wraps beside a menu of one row. A menu that does not fit there, which used to run out of the header sideways, wraps onto further rows. A site whose entries fit beside its title at every width, like the showcase at 1280 pixels, looks as before.

The switch at 1024 pixels is a second breakpoint of the stylesheet, bp.$lg, used by the site header only. Moving it means recompiling the SCSS, like the first one.

Important: The selected tab carries an indicator 

Description 

The selected tab of .theme-tabs - the component, and the tabs content element that renders it - now carries a bar of two pixels in the primary accent along its top edge. It used to be told apart from the other tabs mainly by the colour of its label; its frame is drawn in the decorative border colour and hardly shows. The bar is a shape the other tabs do not have, so the selection no longer depends on colour alone (WCAG 1.4.1).

Every tab carries the top edge of the bar, transparent until it is selected, so selecting a tab paints the bar and moves nothing.

Impact 

The height of a tab is unchanged: it is the minimum height of the tap target, which includes the edge. The colour of the bar is the component token --theme-tabs-indicator-color , its width --theme-tabs-indicator-width ; a site package re-points either in its own CSS. Under forced colours the selected tab keeps the system highlight colour it had before.

Important: Tables are no longer striped by default 

Description 

A table content element used to stripe every other row, whatever its Table class said. The core's Striped therefore changed nothing, and there was no way to get a table without stripes.

The table component now draws its default rows separated by a hairline only. Stripes are the modifier .theme-table--striped , which the Striped table class selects. The table class offers more looks beside it - see Feature: Typography and text components.

Impact 

Every table content element with the default table class loses its stripes, and so does any template of a site package that renders <table class="theme-table"> without a modifier.

A table that should stay striped selects Striped as its table class, or carries theme-table--striped in a template of its own. A site package that wants every table striped again, without touching the records, restores the previous default in its own CSS:

.theme-table > tbody > tr:nth-child(even) > * {
    background-color: var(--theme-table-stripe-background);
}
Copied!

The theme uses no cascade layers, so the site package's stylesheet has to be included after theme.css.

Important: Theme content elements have their own frame 

Description 

The content elements of the theme's own - the Theme group of the Create new content element wizard - are rendered through the TypoScript object lib.themeContentElement instead of lib.contentElement. The classic content elements stay on lib.contentElement.

lib.contentElement is a name the theme shares with fluid_styled_content, and that extension clears the object before it defines it. Loaded after the theme, it took the theme's template paths with it, and every element of the theme's own failed to find its template. An object of the theme's own name is not cleared by anything else, so these elements render the same whether fluid_styled_content is installed or not.

Impact 

Nothing changes for an installation that sets its template paths through the constants: theme.templateRootPath, theme.partialRootPath and theme.layoutRootPath set both objects.

An installation that adds root paths to lib.contentElement directly now reaches the classic content elements only. To override a template of a theme element that way, add the same root path to lib.themeContentElement:

lib.themeContentElement.templateRootPaths.20 = EXT:my_site_package/Resources/Private/Templates/
Copied!

Important: WebVTT files become uploadable 

Description 

The extension appends vtt to $GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'] in its ext_localconf.php. This is a change to what the whole installation accepts as an upload, made by a theme, so it is stated here rather than left in a code comment.

What exactly changes 

Before After
css,csv,htm,html,js,json,md,rst,rtf,sql,srt,tmpl,ts,txt,typoscript,xlf,xml,yaml,yml the same list, with vtt appended

The value is read, split, and written back with one entry added. Nothing the core ships is removed, replaced or reordered, and the entry is added only when it is not already present - so an installation that already allows vtt, through its own additional.php or another extension, is changed in no way at all. The order of the other extensions' additions is preserved, because the list is only ever appended to.

Why this is necessary at all 

Text & Media gained a Captions field for WebVTT caption tracks - see Feature: Audio and video in Text & Media. A caption track is what WCAG 1.2.2 asks of a video, and it is the one part of a media element that is not decoration.

A file reaches a storage only if its extension is in one of the three lists ResourceConsistencyService::getAllowedFileExtensions() reads - textfile_ext, mediafile_ext and miscfile_ext - whenever the security feature security.system.enforceAllowedFileExtensions is on. TYPO3 ships srt, the other subtitle format, in textfile_ext, and vtt in none of the three. Without this change an editor uploading the file the field asks for gets Resource consistency check failed, a message that names neither the file nor the extension, and the field can never be filled. The registration is therefore a prerequisite of the feature, not a convenience for the demo tree.

textfile_ext and not mediafile_ext: a WebVTT file is text - the transcript of a medium, not a medium - and mediafile_ext is what common-media-types resolves to, which is the list the Media field accepts. A caption file has no business being offered there.

Why the extension and not only the development instance 

The narrower option was to make the change in the development instances alone, in their config/system/additional.php, so that only the showcase is affected. That was rejected: it would make the shipped Captions field work in this repository and fail in every installation that uses the extension, with the misleading error above and nothing to point at the cause. A field an extension ships has to work where the extension is installed.

Impact 

An installation using this extension accepts .vtt uploads. That is the intent, and it is the minimum the Captions field needs.

If you do not want it, remove the entry again in your own additional.php, which is loaded after ext_localconf.php:

$extensions = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'], true);
$GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'] = implode(
    ',',
    array_filter($extensions, static fn(string $extension): bool => $extension !== 'vtt'),
);
Copied!

The Captions field then stays in the form and refuses the upload, which is the state the field was in before this change.