
Typo3 Shadcn Content Elements
- 9 installs
- 33 repo stars
- Updated July 27, 2026
- dirnbauer/webconsulting-skills
Build and restyle TYPO3 Content Blocks as a coherent shadcn/ui system using semantic tokens, Fluid atoms, and no hardcoded colors.
About
This skill produces, audits, and overhauls TYPO3 Content Blocks content elements styled with shadcn/ui presets, semantic tokens, and Fluid atoms. A developer uses it to create, restyle, or preview Content Blocks with shadcn/ui and Tailwind v4 tokens.
- Builds TYPO3 Content Blocks styled as a coherent shadcn/ui system with semantic tokens
- Strict no-hardcoded-color rule; consumes Tailwind v4 tokens and shared Fluid atoms
Typo3 Shadcn Content Elements by the numbers
- 9 all-time installs (skills.sh)
- Ranked #1,469 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dirnbauer/webconsulting-skills --skill typo3-shadcn-content-elementsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 9 |
|---|---|
| repo stars | ★ 33 |
| Last updated | July 27, 2026 |
| Repository | dirnbauer/webconsulting-skills ↗ |
What it does
Build and restyle TYPO3 Content Blocks as a coherent shadcn/ui system using semantic tokens, Fluid atoms, and no hardcoded colors.
Files
TYPO3 shadcn Content Elements
Source: https://github.com/dirnbauer/webconsulting-skills
Purpose
Use this skill to build TYPO3 Content Blocks content elements that behave like a coherent shadcn/ui system: preset-driven tokens, shared Fluid atoms/molecules, complete field coverage, useful backend previews, semantic icons, local shadcn registry items, and seed data that makes the styleguide pages credible.
This skill assumes Content Blocks are the source of truth for schema and TYPO3 owns rendering. shadcn/ui is the source for theme tokens, component class contracts, data attributes, states, spacing, borders, radius, and typography.
The styling rule is strict: content elements should not hardcode colors or theme-specific visual values. Raw oklch(), hsl(), rgb(), hex colors, and fixed light/dark assumptions belong in the committed shadcn theme token source only. Templates, element CSS, backend preview CSS, generated icons, and chart scripts should consume semantic tokens or Tailwind/shadcn utility classes.
Desiderio Creation Contract
When creating or overhauling Desiderio shadcn/ui content elements, respect these project-specific rules before general preferences:
- Use TYPO3 14, Content Blocks 2.x, and Fluid 5.3 templates. Frontend entrypoints stay in
templates/frontend.fluid.html(the Content Blocks 2.x default since 2.0.4; plainfrontend.htmlis the legacy 1.x name); backend previews stay intemplates/backend-preview.fluid.html. - TYPO3 owns rendering. Do not copy React, Radix, or Astro framework components into the runtime. shadcn/ui is the source for tokens, class contracts, states, radius, spacing, focus rings, and component anatomy.
- Use Desiderio Fluid atoms/molecules/components where they exist, especially
d:layout.section,d:layout.container,d:atom.typography,d:atom.icon, buttons, badges, cards, form controls, tabs, accordions, tables, alerts, dialogs, and chart wrappers. - Respect responsive design in every element: mobile-first grids, stable dimensions for fixed-format UI, no text overflow, no incoherent overlap, and layouts that still read well at narrow and wide widths.
- Keep designs genuinely polished. Prefer clear editorial/product systems with strong hierarchy, deliberate spacing, real media or useful demo content, and restrained shadcn surfaces. Avoid decorative-only gradients, orphan cards, nested cards, and generic placeholder copy.
- Use only semantic tokens and shadcn/Tailwind classes in templates and CSS. Raw color values are allowed only in theme token files and generated Tailwind output.
- Use XLIFF 2.0 labels with modern stable ids such as
copy_code,preview.uid, orfield.headline. Provide English base labels and German labels for editor-facing strings. Use ICU MessageFormat for variable or pluralized labels instead of concatenating strings. - Use
LLL:references for labels in Content Blocks YAML, backend previews, and templates. Do not hardcode editor-facing labels in Fluid when they belong in XLIFF. - For behavior that pure HTML, CSS, and shadcn classes cannot cover, use the shared Desiderio Astro runtime in
Resources/Public/Js/astro.js. Templates should emit data attributes such asdata-astro-counter,data-astro-reveal,data-astro-highlight,data-astro-copy,data-astro-tilt, ordata-astro-marquee. - Do not add inline
<script>blocks to content element templates. Add idempotent behavior to shared JS include files, register them through TypoScript or AssetCollector, and make reduced-motion behavior explicit. - Code examples should use the editorial code-block element with
data-astro-highlightand the copy control. Keep source text escaped in Fluid and let the runtime decorate tokens after load. - For forms, use TYPO3 Form Framework definitions under
Resources/Private/Forms, TypoScript renderers in the Site Set, andtypo3/cms-formas a Composer runtime dependency. Do not hand-roll mail forms in Fluid templates. - Configure form mail routing through TYPO3 form finishers and Site Settings (
desiderio.forms.*). Use a PSR-14 listener, such asBeforeEmailFinisherInitializedEvent, only to map site-specific sender/receiver options. Do not invent asetup.phpmail mapping layer. - CAPTCHA is opt-in and must be TYPO3 Form-compatible. Prefer honeypot, validation, throttling, and server-side spam checks first. If a real CAPTCHA provider is required, keep provider keys in Site Settings or environment configuration, never in templates, fixtures, or registry items.
- Seed data is part of the design. Every element needs credible English/German demo copy, valid links, meaningful icons, realistic metrics, useful code snippets, structured repeatables, image alt/source data, and select values that match the current
config.yaml. - Verify visually in the browser after changes. For Desiderio styleguide work, check the local DDEV site and the relevant page URL, including the full styleguide/catalog pages, not just the element that was edited.
- Finish with project checks:
php scripts/audit-content-elements.php, PHPUnit/static checks, Tailwind sync, registry build when registry files changed, JS syntax checks when shared JS changed, and a browser pass.
Default Workflow
1. Establish the preset source.
- If a shadcn/create URL or preset id is given, extract the id, for example
b4hb38Fyj. - Run
npx shadcn@latest info --jsonin the project whencomponents.jsonexists. If a TYPO3/non-TS repo fails path resolution, add a smalltsconfig.jsonplus scratch aliases socomponents,ui,lib,utils, andhooksresolve without copying React runtime code into TYPO3. - Use
shadcn createorhttps://ui.shadcn.com/create?item=previewas a design-system source. Use an Astro scratch app when the user asks for Astro/include-file JavaScript patterns; use a Vite/React scratch app only to inspect primitive class contracts. - Do not install React/Astro components as TYPO3 runtime code. TYPO3 owns rendering; shadcn owns tokens, component contracts, and registry metadata.
- Commit the preset as local CSS variables and Site Settings, for example
body[data-shadcn-preset="b4hb38Fyj"]; do not add a runtime preset downloader or binary switcher. - Treat
:rootas the light-mode base and.darkas the dark-mode override, matching shadcn. Do not write content elements that only look correct in one mode. - Read
references/shadcn-preset-workflow.mdbefore changing theme tokens or primitive class strings.
2. Audit before editing.
- Run
php <skill>/scripts/audit-content-elements.php <repo-root>when working on a repo withContentBlocks/ContentElements. - For Desiderio, also run the project audit script (
php scripts/audit-content-elements.php) because it contains repo-specific hard checks for inline styles, raw colors, variant branches, undeclared rendered fields, and seed gaps. - If local PHP cannot load Composer because the extension requires PHP 8.3+, run the audit and PHPUnit through DDEV PHP 8.3. Copy the repo to a temporary path inside the DDEV project first so you do not mutate a dirty vendor checkout.
- Use the output to prioritize missing previews, undeclared rendered fields, unused configured fields, missing labels, missing icons, and repeatable-field gaps.
- Treat the audit as a starting point, then manually inspect high-risk templates.
fixture_missing_fieldandcollection_child_seed_gapcan be soft signals when the project seeder auto-fills missing demo values, but the rendered seeded pages still need a visual pass. - Do not stop after icon-like fields. Loop through every Content Block element in the catalog and classify each issue as schema, template, CSS/token, JavaScript/include, backend preview, fixture, or seed-fallback work.
- Search for structural workaround leftovers before declaring a pass complete:
f:split(, pipe-delimited links such asLabel|https://..., legacy list fields such asfeatures_list,specs_text,row_data, comma-delimitedtier_values, and monolithicshadcn2fluid_*fixture maps. - Classify leftovers carefully: compatibility converters in a seed script can remain only when they convert old input into current structured fields; templates, fixtures, Content Blocks schema, tests, and docs should demonstrate the current structured model.
- Apply the Fluid safety checks from
references/content-element-contract.md, especially for date/time formatting,typolinkattributes, resource paths, and string-only ViewHelpers.
3. Fix the shared component layer first.
- Update
Resources/Private/Componentsatoms/molecules from generated shadcn class contracts before editing hundreds of individual templates. - Prefer shared Fluid components over bespoke content-element CSS.
- Keep semantic shadcn tokens such as
bg-background,text-foreground,bg-card,border-border,ring-ring,text-muted-foreground,bg-primary, andtext-primary-foreground. - Put client-side behavior in shared include files registered through TypoScript,
f:asset.script, or TYPO3's AssetCollector. Templates should emit data attributes and JSON payload attributes; they should not contain inline<script>renderers. - For charts and interactive patterns, mirror the shadcn component contract: data attributes, CSS variables such as
--chart-1through--chart-5, idempotent initialization, and no hardcoded color fallbacks. - Read
references/styling-token-contract.mdbefore adding or changing element CSS, SVG icon colors, chart colors, shadows, overlays, or backend preview styling.
4. Keep the local shadcn registry valid.
- If the project exposes Desiderio/shadcn patterns for reuse, keep a root
registry.jsonand build output under a public registry endpoint such asResources/Public/ShadcnRegistry. - Add a
components.jsonregistry namespace such as"@desiderio": "Resources/Public/ShadcnRegistry/{name}.json". - Add scripts such as
shadcn:infoandregistry:build, then run both after changing tokens, runtime assets, or registry item files. - Use current shadcn CLI commands as the source of truth:
shadcn info --jsonfor project configuration andshadcn build --output <dir>for registry output. The upstream registry is framework-agnostic, so a TYPO3 registry item may package Fluid, TypoScript, CSS token sources, JavaScript include files, and Site Settings when the item type describes that contract. - Registry items should package theme token sources, Tailwind source files, Site Settings, TypoScript asset includes, and shared runtime assets. Do not put generated Tailwind output or one-off page CSS into a registry item unless consumers actually need that build artifact.
5. Audit each content element against its contract.
- Read
references/content-element-contract.md. - For every element, compare
config.yaml,templates/frontend.fluid.html,language/labels.xlf,assets/icon.svg, CSS/JS assets, backend preview, and seed coverage. - Treat
config.yaml:titleas an editor-facing product name. Prefer names likeText & Media,Image Call to Action, andLogo Cloud Heroover raw slugs such astextmedia,CTA With Image, orHero Logo Cloud. - Use the
typo3-translationsskill for XLIFF format choices, English/German localization, ICU MessageFormat strings,LLL:references, and localized wizard group labels. - Generate element descriptions from the element purpose and schema so each description explains what editors can build. Do not reuse a single generic sentence across the catalog.
- Every configured editor field should be rendered or intentionally marked backend-only.
- Every rendered field should exist in
config.yaml. - Every Collection child field should be rendered or intentionally omitted.
- Treat editor
Style/variantfields as contracts, not decorative metadata. Keep one only when the value is passed to an existing shadcn-backed Fluid component feature (for example Button/Badge/Alert variants or TabsListdefault/line) or when the template has a real layout/behavior branch. Do not keepvariantfields that only append unused BEM modifier classes. - Prefer removing unsupported style fields over inventing bespoke visual variants. If a style option cannot be expressed by the shared shadcn atoms/molecules or existing shadcn class contracts, remove the field from
config.yaml, frontend templates, backend previews, and fixtures. - If a repo-level test requires every content element to keep
assets/frontend.css, use an empty/comment-only marker file when the shared component layer handles all styling. Do not re-addf:asset.cssincludes or style rules merely to satisfy the file-presence contract. - Use nested
Collectionfields for second-level repeatables when the installed Content Blocks version supports nested Collections; keep seed scripts recursive so child rows are created after their parent IRRE rows. - Give every
Collectionfield an explicittable:key. Without it, Content Blocks derives the table from theidentifieralone, so unrelated elements that pick the same name (rows,posts,column_items,cells,items,links...) silently share one physical table — schema migrations break,foreign_table_parent_uiddrifts to the wrong type, and renames lose data. Names like<contentblock-slug>_<identifier>are safe, for exampleblog_teasers_postsorfeature_matrix_rows. Seereferences/content-element-contract.md"Collection Table Naming". - Use TYPO3
Linkfields for editor-managed URLs. Keep visible link text in a separate text field and never encodeLabel|https://...pairs in textareas. - File fields need alt text and copyright/source strategy in seed data and previews.
- Date and time fields must be formatted to strings before they are passed to visual-editor text rendering or HTML attributes.
6. Add backend previews for the page/layout module.
- Read
references/backend-preview-pattern.md. - Add
templates/backend-preview.fluid.htmlusing Content BlocksPreviewlayout withContentand optionallyFooter. - Keep
<f:section name="Header"></f:section>empty — never render a duplicate headline above the formatted card. The card itself shows the title insided-ce-preview__title. Removing the section instead of emptying it triggers TYPO3'sInvalidSectionExceptionfallback to the standard renderer, which re-adds the duplicate. - Inside
d-ce-preview__meta, surface the editor-facing element name, the Content Block identifier, the recorduid, and the pagepidas small pills. Use<f:translate key="LLL:EXT:desiderio/Resources/Private/Language/labels.xlf:preview.uid"/>and:preview.page— never hardcode the labels. - Show the most important data: title/headline, summary, bullets/repeatables, CTA/link, chart metrics, and thumbnails.
- Never rely on TYPO3’s Core fallback preview for custom Content Blocks.
7. Create or refresh icons.
- Read
references/icon-pattern.md. - Each element gets a semantic
assets/icon.svg. - Use TYPO3-style 16x16 SVGs: transparent background, root color
var(--icon-color-primary,currentColor),currentColorprimary paint,var(--icon-color-accent,currentColor)accent, readable in light and dark. - For large catalogs, actively prevent look-alike icons: audit normalized SVG bodies with titles removed, inspect same-family groups such as heroes/pricing/nav/footers/features, and add per-element geometry instead of relying on one family template.
- If the user asks for GPT Image / Image GPT /
gpt-image-*, first verify the currently available OpenAI image model and API credentials. Use image generation for metaphor boards or visual direction; redraw the final asset as clean, editable SVG because TYPO3 backend icons need tiny, token-colored vectors. - Treat Lucide, Tabler, and TYPO3 backend icons as metaphor references, not as a reason to reuse the same glyph across many content elements.
8. Update seed scripts.
- Fill all top-level and repeatable fields for every element.
- Fill nested repeatables as structured arrays; never collapse a second-level list into a newline-separated textarea when an IRRE Collection is available.
- Add real dummy images from Unsplash or committed demo assets, with alt text and copyright/source fields where available.
- Seed links as structured data or distinct fields, for example
{"label": "Docs", "link": "https://example.com/docs"}orlink_1_labelpluslink_1. Do not generate pipe-delimited link strings. - Seed chart/data elements with valid JSON that the frontend template actually parses.
- When the seeder auto-fills omitted optional fields, inspect the generated pages anyway. Empty fixtures are acceptable only when the fallback content is specific enough for visual QA and editor demos.
- If a full-catalog audit reports many
fixture_missing_fieldorcollection_child_seed_gaprows but hard template/style checks are clean, improve the project seeder first. Fallback copy should read like real pattern-library content (Pattern Library, realistic product/demo copy, useful CTA labels, valid links, valid icon keys, token names such asprimary) and must not expose generic placeholder phrases such asComplete demo content. - Prefer shadcn token
Selectfields over freeformColorfields for elements whose appearance should track theme changes. If an element needs editor color choice, render it through token-backed data attributes or CSS variables and keep raw color values out of fixtures and fallback seed data. - Validate seeded
Selectvalues against the currentconfig.yamlitems before insertion. Stale fixture values from removed style variants should fall back to the configured default instead of seeding inert editor choices. - Keep Desiderio fixtures in each
ContentBlocks/ContentElements/<element>/fixture.jsonfile. Do not create or restore monolithicshadcn2fluid_*fixture mappings. - If old fixture keys must be accepted for migration, convert them at seed time into current fields or nested Collections and keep those aliases out of new fixtures and examples.
9. Verify and commit in reviewable slices.
- Run CSS build, PHP unit/static checks, and Content Blocks/TYPO3 cache validation.
- Run
npx shadcn@latest info --jsonandnpx shadcn@latest build --output <public-registry-dir>whencomponents.jsonorregistry.jsonchanged. - Scan content elements and shared generated preview code for raw color literals; allowed raw preset values should be confined to the shadcn theme token file and generated Tailwind output.
- Scan templates for inline scripts when shared JavaScript was introduced:
rg -n "<script>|JSON\\.parse" ContentBlocks/ContentElements/*/templates/frontend.fluid.html. - For a user-requested full pass, run the complete content-element audit loop repeatedly over all elements, not just changed files. Ten iterations are acceptable when the user explicitly asks to loop 10 times; report the stable summary and the element count.
- For large overhauls, commit by layer: preset/theme, primitives, generated previews, icons, per-element fixes, seed data.
- Browser-check frontend pages and TYPO3 backend layout module previews.
Critical Rules — Collection Table Naming
This is the single most damaging mistake when authoring Content Blocks. Read this before creating any `type: Collection` field.
Every type: Collection field generates a separate physical database table. Without an explicit table: key, Content Blocks falls back to using the bare identifier as the table name. That breaks production in three escalating ways:
1. Reserved-word collisions. Identifiers like rows, groups, order, range, system, values, user are MySQL/MariaDB reserved words. Doctrine quotes at runtime so the table works inside TYPO3, but mysqldump, mysqlcheck, ad-hoc CLI queries, schema-diff tools, and IDE introspection all stumble on the unquoted name. 2. Cross-element table sharing. When two Content Blocks both pick a common identifier (posts, column_items, rows, cells, items, links, features, tabs, members, slides, stats, tiers, partners, clients, reviews, routes, metrics, specs, jobs, awards...), Content Blocks merges their schemas into one shared table. The shared table becomes the union of every consuming element's columns, schema migrations get confused, and renaming one block later requires a parent-aware data migration to split records back apart. 3. `foreign_table_parent_uid` type drift. Older Content Blocks versions created the parent reference column as varchar(255) DEFAULT '' NOT NULL. Newer TCA-driven schema migration wants int unsigned DEFAULT 0 NOT NULL. The migration fails with Truncated incorrect INTEGER value: '' because existing rows hold empty-string values that strict-mode MySQL refuses to coerce. Recovery requires UPDATE <table> SET foreign_table_parent_uid = 0 WHERE foreign_table_parent_uid = '' before the ALTER. Adding table: from the start avoids the legacy column path entirely on fresh tables.
Don't
- Do not declare a
type: Collectionfield withouttable:. - Do not use bare names like
rows,posts,column_items,cells,items,links,features,tabs,members,slides,stats,tiers,groups,users,range,order,system,valuesas atable:value — they collide with other extensions or with reserved SQL keywords. - Do not assume "it works in TYPO3" means it is safe — Doctrine's identifier quoting hides the problem until a dump, restore, or schema migration uncovers it.
- Do not rename a Collection's
table:after editor data exists without a parent-aware data migration: dropping a shared table loses every block's content, and renaming without migration orphans the rows.
Do
- Give every
type: Collectionfield an explicittable:directive in the YAML. - Use the pattern
<contentblock-slug-as-snake_case>_<collection-identifier>, for exampleblog_teasers_posts,feature_matrix_rows,footer_mega_column_items,pricing_plan_features,data_table_cells. - For nested Collections, fold the parent collection name into the child's table name (
pricing_plan_featuresfor afeaturesCollection nested insideplans). - When auditing or porting an existing extension, add
table:to every Collection in the same pass — schema migrations regress as soon as one block falls through. - Before declaring a Collection sweep complete, run the audit grep below.
Audit
# Every Collection field that lacks an explicit table: directive (should print nothing).
rg -nP '^\s*type: Collection\s*$' --no-heading -A1 ContentBlocks/ContentElements \
| rg -B1 -v 'table:'
# Bare table names that match MySQL/MariaDB reserved words.
rg -n '^\s+table:\s+(rows|groups|range|order|system|values|user|cross|dense_rank|window)\s*$' \
ContentBlocks/ContentElements
# Bare unprefixed names — risky for cross-extension collision even if not reserved.
rg -nP '^\s+table:\s+[a-z][a-z0-9_]*\s*$' ContentBlocks/ContentElements \
| rg -v '_[a-z]' # everything kept after this filter has no underscore — review by hand.The full reasoning, migration playbook for an already-shared table, and additional examples live in references/content-element-contract.md "Collection Table Naming".
Quality Bar
- Preset changes must visibly change design through committed tokens and shared component classes.
- Content elements should look like shadcn/ui, not generic Bootstrap or one-off CSS.
- Content elements must be light/dark capable because they consume semantic tokens rather than fixed color values.
- Backend previews should help editors recognize content without opening the form.
- Icons should form a coherent family, not random drawings, and every content element should remain distinguishable in the new content element wizard at 16px.
- Generated or bulk changes need deterministic scripts and tests where possible.
- Active content elements must not depend on
shadcn2fluid; allowed mentions are limited to package conflicts, migration notes, and tests/docs that explicitly prevent the old package or fixture map from returning. - Every
type: Collectionfield has an explicittable:key with a<contentblock-slug>_<identifier>value. No bare names, no reserved SQL words. Audit grep in "Critical Rules — Collection Table Naming" should print nothing.
References
references/shadcn-preset-workflow.md: scratch-app preset workflow and token expectations.references/styling-token-contract.md: no-hardcoded-style rules, light/dark behavior, and verification scans.references/content-element-contract.md: field/template/label/seed audit contract.references/backend-preview-pattern.md: Content Blocks backend preview pattern.references/icon-pattern.md: TYPO3 backend icon rules.../typo3-translations/SKILL.md: XLIFF, localization, ICU MessageFormat, andLLL:label rules.
Scripts
scripts/audit-content-elements.php <repo-root>: JSON audit for Content Blocks directories.
interface:
display_name: "TYPO3 shadcn Content Elements"
short_description: "Produce TYPO3 Content Blocks styled with shadcn/ui presets."
default_prompt: "Use this skill to audit, generate, or overhaul TYPO3 Content Blocks content elements with shadcn/ui preset tokens, Fluid atoms/molecules, icons, backend previews, and seed data coverage."
Backend Preview Pattern
Content Blocks custom Content Elements should provide their own backend preview. TYPO3's default preview renderer is optimized for Core fields such as header, subheader, and bodytext, so custom schemas can produce broken or empty layout module previews.
Required Shape
Create:
ContentBlocks/ContentElements/<element>/templates/backend-preview.fluid.html
Use:
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:cb="http://typo3.org/ns/TYPO3/CMS/ContentBlocks/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:layout name="Preview"/>
<f:section name="Header"></f:section>
<f:section name="Content">
<f:asset.css identifier="desiderio-content-preview" href="EXT:desiderio/Resources/Public/Css/content-preview.css"/>
<div class="d-ce-preview" data-slot="card">
<div class="d-ce-preview__meta">
<span class="d-ce-preview__type" data-slot="badge">Element title</span>
<f:if condition="{settings._content_block_name}"><span class="d-ce-preview__ctype">{settings._content_block_name}</span></f:if>
<span class="d-ce-preview__ctype"><f:translate key="LLL:EXT:desiderio/Resources/Private/Language/labels.xlf:preview.uid"/>: {data.uid}</span>
<span class="d-ce-preview__ctype"><f:translate key="LLL:EXT:desiderio/Resources/Private/Language/labels.xlf:preview.page"/>: {data.pid}</span>
</div>
<f:if condition="{data.header}">
<h3 class="d-ce-preview__title">{data.header}</h3>
</f:if>
<!-- concise editor-facing summary fields -->
</div>
</f:section>
</html>Header Section Stays Empty
Leave <f:section name="Header"></f:section> empty. The page-module preview chrome already renders the language flag, content-type icon, and edit link above the card; emitting another headline above the formatted card duplicates the title (the formatted card already shows {data.header} inside d-ce-preview__title).
The empty section is intentional and required: removing the section entirely throws InvalidSectionException, which TYPO3 catches and falls back to the standard renderer — re-introducing the duplicated header. An empty body returns "" after trim(), which suppresses the slot cleanly without triggering the fallback.
Meta Pills (UID + Page)
The first row inside the card uses d-ce-preview__meta to host small pills that help editors orient inside the page tree without opening the record:
d-ce-preview__type— uppercase info badge with the editor-facing element title (for exampleFounder Quote).d-ce-preview__ctype— monospace pill for the Content Block name ({settings._content_block_name}), the recorduid, and the recordpid.
The UID and page pills must use XLIFF labels via <f:translate key="LLL:EXT:desiderio/Resources/Private/Language/labels.xlf:preview.uid"/> and :preview.page. The keys live in the global desiderio Resources/Private/Language/labels.xlf file (and every translated companion de.labels.xlf, es.labels.xlf, fr.labels.xlf, it.labels.xlf, hu.labels.xlf). Do not hardcode UID / Page strings into the template.
What To Show
Show the highest-signal content:
- headline/title,
- eyebrow/category/status,
- short description/body excerpt,
- CTA text/link,
- repeatable item count plus first few labels,
- image thumbnails,
- chart/data summaries,
- table dimensions,
- formatted date and time values instead of raw objects,
- warnings when required data is empty.
Visual Style
Match TYPO3 backend expectations:
- compact bordered card,
- internal padding,
- readable in dark and light backend schemes,
- small thumbnails with rounded borders,
- bullet lists for repeatables,
- no heavy frontend hero layout.
Avoid
- Copying the full frontend template into the backend preview.
- Calling fields that do not exist on that element, including
data.headeron headerless blocks. - Relying on
headerfor elements that do not declare it. - Loading frontend-only JavaScript just to show a preview.
Content Element Contract
Use this checklist for every ContentBlocks/ContentElements/<element>.
Required Files
config.yamltemplates/frontend.fluid.html(Content Blocks 2.x default since 2.0.4; plainfrontend.htmlis the legacy 1.x name)templates/backend-preview.fluid.htmllanguage/labels.xlfassets/icon.svgassets/frontend.cssonly when the shared Fluid component layer cannot express the layout, unless the repository has an explicit per-element asset-file invariant; then keep an empty/comment-only marker file and do not include it from the template until rules are actually neededassets/frontend.jsonly when interaction or visualization is genuinely element-local; prefer a shared include file for whole families such as charts, carousels, tabs, counters, and dashboards
Schema And Template Checks
config.yaml:title is the editor-facing content element name. Use clear product names instead of slug-shaped labels: Text & Media, Image Call to Action, Logo Cloud Hero, Table of Contents Navigation.
For each field in config.yaml:
- If it is editor-facing, render it in
frontend.fluid.htmlor document why it is backend-only. - If it is
Select,Checkbox, variant, density, or layout related, set a default. - If it is named
variantor labeled asStyle, prove that every value changes output through an existing shared shadcn component feature or a real template branch. Good examples are Button/Badge/Alert variants and TabsListdata-variant="default|line". Bad examples are values that only produce unused classes such asaccordion--bordered,table--striped, ortabs--pills. - Remove unsupported style fields instead of adding one-off CSS. A content element should not invent custom visual variants merely because a
variantfield exists. - If it is
Collection, render meaningful child fields and handle empty states. - If it is
Collection, give it an explicittable:key. Without one, Content Blocks derives the table name from the fieldidentifieronly, which silently shares a single physical table across every content element that uses the same identifier (posts,rows,column_items,cells,items,links...). The shared table accumulates the union of all those blocks' fields, schema migrations get confused (parent UIDs end up as the wrong type, see "Collection Table Naming" below), and a future rename loses or scrambles content. Pick a name like<contentblock-slug>_<identifier>(for exampleblog_teasers_posts,feature_matrix_rows,footer_mega_column_items). - If it is a repeatable list inside another repeatable item, use a nested
Collectionwhen the installed Content Blocks version supports it. Do not model editor-managed feature lists as newline-separated textareas just because they are second-level data. - Do not model repeatable editor content as delimiter protocols. Fields such as
features_list,specs_text,row_data, comma-separatedtier_values, or newline/pipe encoded people/pages are legacy migration inputs, not the target schema. - If it is
File, render with TYPO3 file APIs and provide alt/caption/copyright strategy. - If it stores an editor-managed URL, use
type: Link; keep the visible label in a separate text field such ascta_text,link_1_label, orchild_1_label. - If it is JSON/data, validate that the template emits a stable
data-*attribute and the shared JavaScript parses the declared structure.
For each field used in frontend.fluid.html:
- It must exist in
config.yaml, unless it is a TYPO3 system field likeuid. - Use
f:render.text(field: ...)for Content Blocks transformed text fields where appropriate. - Use
f:format.htmlonly for configured rich text. - Format
Date,DateTime, andTimefields explicitly withf:format.datebefore passing them to attributes or visual-editor text helpers. Do not render rawDateTimeImmutableobjects through text ViewHelpers. - Since Fluid 2.12 (TYPO3 v12+), tag-based ViewHelpers such as
f:link.typolinkaccept arbitrary unregistered attributes (for examplearia-labeloraria="{...}") directly;additionalAttributesstill works but is not required. Pick one style per project and use it consistently. - Render
Linkfields withf:link.typolink parameter="{field}"after checking{field.url}. The field resolves to a TYPO3 typolink object with URL, target, class, title, and additional params. - Do not parse link textareas with
f:split(separator: '|'). A pipe-delimited string such asDocs|https://example.com/docshides URL semantics from TYPO3, weakens editor UX, and breaks previews/seed conversion. - Do not parse structural lists with
f:split()in frontend templates. If a template needs rows, cells, features, people, pages, specs, or tier values, the data should arrive as a Collection or JSON/data field with an explicit parser. - Use package-qualified resource paths such as
EXT:desiderio/Resources/Public/...forf:uri.resource; a bare relative path can be parsed as an empty package key. - Guard string-only ViewHelpers such as
f:splitwith defaults or conditions when the editor field can be empty, an array, or parsed JSON. - Avoid undeclared Fluid arguments and unsupported ViewHelper arguments.
Client-Side Behavior Pattern
Use this pattern for charts, dashboards, carousels, counters, and other browser behavior:
- Register shared JavaScript through TypoScript
page.includeJSFooterlibs,f:asset.script,
or TYPO3's AssetCollector.
- Keep Fluid templates declarative: semantic markup, shadcn classes,
data-ce,
data-chart-data, data-chart-json, data-state, and ARIA attributes.
- Do not render inline
<script>blocks insidetemplates/frontend.fluid.html. - Make initializers idempotent by marking enhanced nodes, for example
data-chart-rendered="true".
- Read colors from CSS variables such as
--chart-1,--chart-2,--primary,
--muted-foreground, or currentColor; do not put raw color literals in JS.
- If the user asks for Astro include files, use an Astro shadcn scratch app to model the
component behavior, then ship compiled/shared include assets in TYPO3.
Run this scan after moving scripts:
rg -n "<script>|JSON\\.parse" ContentBlocks/ContentElements/*/templates/frontend.fluid.htmlLink Field Pattern
Use this pattern whenever editors need a link:
- identifier: cta_text
type: Textarea
rows: 1
label: Button Text
- identifier: cta_link
type: Link
label: Button Link<f:if condition="{data.cta_text} && {data.cta_link.url}">
<f:link.typolink parameter="{data.cta_link}" class="...">
{data -> f:render.text(field: 'cta_text')}
</f:link.typolink>
</f:if>For repeatable link groups, prefer a flat Collection with one row per link when the model allows it. If the element already uses a Collection for groups and Content Blocks cannot nest another Collection inside it, use fixed slots:
- identifier: link_1_label
type: Textarea
rows: 1
label: Link 1 Label
- identifier: link_1
type: Link
label: Link 1Do:
- Store URL targets in
type: Linkfields so TYPO3 can manage page, file, record, external, target, class, and title data. - Store visible link copy separately. Visible text is the primary accessible name.
- Add
aria-labelonly when a link truly needs one; pass it directly as an attribute (Fluid 2.12+, TYPO3 v12+) or viaadditionalAttributes— both work. - Use image/File
alternativefields for image alt text. - Seed structured links as objects, for example
{"label": "Docs", "link": "https://example.com/docs"}.
Do not:
- Do not use textarea rows like
Docs|https://example.com/docs. - Do not invent
altfields for plain links.altbelongs to images, not anchors. - Do not add redundant
aria-labelvalues that merely repeat the visible link text; the visible text is already the accessible name. - Do not render raw URL strings as visible content unless the URL itself is the intended label.
Nested Collection Pattern
Content Blocks can model second-level repeatables with nested Collections. Use this for structures such as pricing plans with repeatable features:
- identifier: plans
type: Collection
labelField: name
fields:
- identifier: name
type: Textarea
rows: 1
label: Plan Name
- identifier: features
type: Collection
table: pricing_plan_features
labelField: text
fields:
- identifier: text
type: Textarea
rows: 1
label: Feature<f:for each="{data.plans}" as="plan">
<f:for each="{plan.features}" as="feature">
{feature -> f:render.text(field: 'text')}
</f:for>
</f:for>Seed fixtures should stay structured:
{
"plans": [
{
"name": "Business",
"features": ["Unlimited users", "Priority support"]
}
]
}If a project is pinned to a Content Blocks release that cannot nest Collections, use an explicit fallback such as fixed feature_1...feature_6 fields and document that limitation in the element.
Collection Table Naming
Every `type: Collection` field must declare an explicit `table:`. This is non-negotiable. The default behavior of deriving the table name from the field identifier is unsafe for three reasons.
Reserved SQL words. MySQL 8.0+ reserves ROWS (used in window-function ROWS BETWEEN); MariaDB matches. Doctrine quotes identifiers at runtime, so the table works in TYPO3, but mysqldump, ad-hoc CLI queries, schema-diff tools, IDE introspection, and mysqlcheck all stumble on unquoted rows. Other risky bare names: groups, order, range, system. Prefix every Collection table to sidestep the reserved-word list permanently.
Cross-element collisions. Several content elements naturally pick the same identifier (posts, rows, column_items, cells, items, links, features, tabs). Without table:, Content Blocks merges them into one shared physical table whose schema is the union of every consuming element's fields. This causes:
- Bloated tables with link/file/select columns that only one of the sharing elements actually uses.
- Schema-migration deadlocks when one of the elements is renamed or removed: TYPO3 wants to drop columns that another element still needs.
- Type drift on
foreign_table_parent_uid: because every old Content Blocks version generated different defaults for the parent reference, a sharedcolumn_itemstable can keep an oldvarchar(255) DEFAULT '' NOT NULLdefinition while TYPO3 wantsint unsigned DEFAULT 0 NOT NULL. The migration then fails withTruncated incorrect INTEGER value: ''because existing rows hold empty strings. - Editor data loss on rename: splitting one shared table back into per-element tables requires a parent-aware data migration, and orphaned rows are common.
TYPO3 convention. Extension-owned tables should be prefixed so dump files, schema diffs, and other extensions can attribute ownership. Bare names like posts collide with every other extension that ships a blog.
Don't
- Do not declare
type: Collectionwithouttable:. The bare-identifier fallback is a footgun, not a feature. - Do not use reserved SQL keywords as table names:
rows,groups,range,order,system,values,user,cross,dense_rank,window. Doctrine quotes at runtime, butmysqldump,mysqlcheck, schema-diff tooling, and IDE introspection break. - Do not use generic single-word table names (
posts,items,cells,tabs,members,slides,stats,links,features,tiers,partners,clients,reviews,routes,metrics,specs,jobs,awards,events,mentions,counters...). They collide with other extensions on the same TYPO3 instance and across the Content Blocks ecosystem. - Do not silently rename a Collection's
table:after editor content exists. Renaming triggers a drop-and-create in TYPO3's schema migration unless you do a parent-aware data move first. - Do not paper over a
Truncated incorrect INTEGER value: ''error by lowering MySQL'ssql_mode. Fix the data (UPDATE ... SET foreign_table_parent_uid = 0 WHERE foreign_table_parent_uid = '') and the column type properly. - Do not assume "the table works because TYPO3 boots fine" means safe — the failure surface is dump/restore, schema migration, and renames, not steady-state runtime.
Do
- Always set
table:on everytype: Collectionfield, top-level and nested. - Name tables
<contentblock-slug-as-snake_case>_<collection-identifier>. Examples:blog_teasers_posts,feature_matrix_rows,footer_mega_column_items,pricing_plan_features,data_table_cells,team_department_members. - For nested Collections, prefix with the parent collection so the relationship is readable (
pricing_plan_featuresinside aplansCollection that has afeatureschild Collection). - When porting or auditing an extension, fix every Collection in one pass — partial fixes leave broken schema transitions.
- Run the audit grep below before declaring a Collection sweep complete.
- Match an existing extension's prefix style if one already dominates (
<ext>_<slug>_<identifier>for stricter projects, plain<slug>_<identifier>for desiderio-style).
Naming Pattern
Use <contentblock-slug-as-snake_case>_<collection-identifier>:
# ContentBlocks/ContentElements/blog-teasers/config.yaml
- identifier: posts
type: Collection
table: blog_teasers_posts
# ContentBlocks/ContentElements/table-content/config.yaml
- identifier: column_items
type: Collection
table: table_content_column_items
- identifier: rows
type: Collection
table: table_content_rows
# ContentBlocks/ContentElements/feature-matrix/config.yaml
- identifier: rows
type: Collection
table: feature_matrix_rows
# ContentBlocks/ContentElements/footer-mega/config.yaml
- identifier: column_items
type: Collection
table: footer_mega_column_itemsFor nested Collections, follow the same pattern with the parent collection in the name (pricing_plan_features, data_table_cells).
Audit Command
# Find Collection fields that lack an explicit table: key.
rg -nP '^\s*type: Collection\s*$' --no-heading -A1 ContentBlocks/ContentElements \
| rg -B1 -v 'table:'
# List bare table names that collide with MySQL/MariaDB reserved words.
rg -n '^\s+table:\s+(rows|groups|range|order|system|values|user|cross|dense_rank)\s*$' \
ContentBlocks/ContentElementsMigration From An Existing Shared Table
If column_items, rows, posts, or similar already exist in production:
1. Add table: to every Content Block that points at the shared name. 2. Inspect the shared table's foreign_table_parent_uid and pid to map each row to its owning content element (usually by joining against tt_content.CType on the parent UID). 3. Move rows into the new per-element tables before running TYPO3's schema migration; otherwise the schema migration drops the shared table and editor content is lost. 4. If foreign_table_parent_uid is varchar(255) with empty-string rows, run UPDATE <table> SET foreign_table_parent_uid = 0 WHERE foreign_table_parent_uid = '' before the ALTER, or strict-mode MySQL will refuse the type change with Truncated incorrect INTEGER value: ''.
Legacy Workaround Cleanup
When auditing an existing set of content elements, replace these workaround shapes with explicit schema:
features_listtextareas ->featuresCollection with one row per feature.specs_texttextareas ->specsCollection with one row per spec.row_datapipe strings -> row Collection with nestedcellsCollection.- comma-delimited
tier_values-> nestedtier_valuesCollection. - newline/pipe encoded
members,people, orpages-> nested Collections with named child fields. Label|URLvalues -> TYPO3Linkfields plus separate label fields.
Frontend templates should render the structured fields directly. Seed scripts may keep old aliases only as one-way compatibility converters, and those converters must normalize legacy values into current fields before inserting records. Do not keep legacy examples in tests, fixture files, docs, or generated templates.
Desiderio Fixture Source
For EXT:desiderio, demo content belongs beside the element:
ContentBlocks/ContentElements/<element>/fixture.jsonDo not create or restore a monolithic shadcn2fluid_* fixture map. Those keys belong to the predecessor package and should not drive current Content Blocks seeding. The seed script should load fixtures by the current desiderio_* CType and then complete any missing fields from the element schema.
Allowed shadcn2fluid references are limited to package conflict metadata, historical migration/spec notes, and tests or documentation that explicitly prevent runtime reuse. Runtime loaders, seed fixtures, Content Blocks, Fluid templates, and generated examples must use current desiderio_* names and per-element fixtures.
Labels
- Use the
typo3-translationsskill for XLIFF version selection, English/German
localization, ICU MessageFormat strings, target state handling, LLL: references, and XML validation.
- Keep global generic labels in
Resources/Private/Language/labels.xlf. - Add element-specific labels in the element
language/labels.xlfwhen the global label is too vague. - Collection item labels should describe editor intent, not implementation names.
- Content element titles should be unique editor-facing names. Descriptions should explain the element's purpose and important editor controls; avoid repeated boilerplate such as "A shadcn/ui styled TYPO3 content element...".
- Custom wizard group names must be registered through TCA with localized
LLL:labels. Keep machine group ids stable, but make visible names descriptive, for exampleHero & Landing Introsinstead ofhero.
Seed Data
Seed scripts must fill:
- all top-level fields,
- all repeatable Collection fields,
- nested Collection fields, seeded as nested arrays and inserted after parent rows,
- image/file fields with usable demo images,
- alt text and copyright/source text when fields exist,
- valid links and CTA labels,
- link fields as TYPO3 links, with labels in separate fields and no
Label|URLsyntax, - date and time fields as values that TYPO3 stores and Fluid formats predictably,
- valid chart/data JSON matching the template parser.
- Select values that still exist in the current
config.yaml; seed commands should normalize or drop legacy style values before inserting rows.
After seeding, run targeted checks for known regressions:
rg -n "f:split\\(|\\|https?://|one per line|features_list|row_data|specs_text|shadcn2fluid" \
ContentBlocks Classes Resources README.md Tests -SReview any matches manually. Compatibility aliases inside seed conversion code can be acceptable; active templates, fixtures, schema, and examples should be clean.
Styling
- Use shared Fluid atoms/molecules first.
- Use shadcn semantic utilities and tokens.
- Use bespoke CSS only for layout geometry, media aspect ratios, chart drawing, or unusual responsive behavior.
- Avoid hardcoded color palettes when a token exists.
- Do not add raw hex, RGB(A), HSL(A), or OKLCH values to live content-element templates/assets.
- Keep raw shadcn preset values in the theme token file; consume them through
var(--*)tokens or Tailwind/shadcn utilities. - Verify both light mode (
:root) and dark mode (.dark) behavior when adding surfaces, borders, overlays, shadows, or chart colors.
TYPO3 Content Element Icon Pattern
Use this reference when creating or refreshing assets/icon.svg for Content Blocks.
SVG Rules
- Use
viewBox="0 0 16 16". - Keep a transparent background.
- Primary geometry uses
currentColor. - Accent geometry uses
var(--icon-color-accent,currentColor). - Include a short
<title>matching the content element, for examplePricing Calculator icon. - Avoid hardcoded black, white, gray, or theme-specific fills.
- Keep detail low; icons must work at 16px in the new content element wizard and page module.
- Make every content element icon visually distinguishable from every other icon in the same wizard group. A title-only or metadata-only difference does not count.
- For large catalogs, normalize SVGs by removing
<title>and compare geometry hashes. Duplicate bodies should fail the pass. - Add a visible per-element signature mark or secondary motif when many elements share a family shape, but do not rely on the mark alone when a stronger metaphor exists.
Starter Template
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round">
<style>.accent{stroke:var(--icon-color-accent,currentColor);fill:none}.accent-fill{stroke:none;fill:var(--icon-color-accent,currentColor)}</style>
<!-- semantic shape -->
</svg>For batches of dozens or hundreds of icons, generate the SVGs deterministically from the content element slug, title, group, and schema.
When a user asks for GPT Image / Image GPT / gpt-image-* icon generation:
1. Check whether OPENAI_API_KEY or the project's image-generation integration is available. 2. Verify the current model name in official OpenAI docs. Do not assume a requested model such as gpt-image-2 exists; use the latest available GPT Image model when configured. 3. Use the image model to produce 2-4 metaphor directions per difficult icon family, for example "pricing annual/monthly toggle", "navbar sticky", or "hero startup". 4. Redraw the chosen direction as SVG paths by hand or deterministic code. Do not commit PNG/WebP output, bitmap traces, base64 images, or unreviewed generated SVG. 5. Sanitize the final SVG: no scripts, no external references, no embedded raster data, no hardcoded colors, no IDs that collide across icons.
Semantic Families
- hero/header: distinguish startup, search, video, testimonial, product, pricing, logo-cloud, countdown, form, fullscreen, gradient, and minimal variants.
- content/text: distinguish article, story, text-media, resource library, case study, highlight, divider, and carousel variants.
- card/grid: distinguish card, overlay, interactive, category, product, library, bento, and matrix variants.
- data/chart: distinguish bar, line, area, pie, donut, radar, heatmap, table, dashboard, KPI, counter, and leaderboard variants.
- navigation/footer: distinguish menu, tabs, table of contents, breadcrumb, pagination, mobile, sidebar, sticky, transparent, mega menu, app links, newsletter, social, contact, and legal footer variants.
- commerce/pricing: distinguish two/three/four tier, toggle, annual/monthly, usage meter, calculator, comparison, enterprise, FAQ, bundle, and order summary variants.
- people/team/testimonial: distinguish member, grid, department, org chart, founder quote, culture, history, values, career, testimonial video, review carousel, rating, awards, certifications, logos, and trust badges.
- legal/compliance: distinguish shield, lock, accessibility figure, document, warning, GDPR/privacy, imprint, terms, and disclaimer variants.
Duplicate Audit
Use this pattern after regenerating icons:
php -r '
$groups = [];
foreach (glob("ContentBlocks/ContentElements/*/assets/icon.svg") as $file) {
$svg = file_get_contents($file);
$svg = preg_replace("#<title>.*?</title>\s*#s", "", $svg);
$groups[sha1($svg)][] = $file;
}
$dupes = array_filter($groups, fn($files) => count($files) > 1);
echo "unique bodies: " . count($groups) . " / " . count(glob("ContentBlocks/ContentElements/*/assets/icon.svg")) . PHP_EOL;
echo "duplicate groups: " . count($dupes) . PHP_EOL;
'If duplicate bodies remain, add a more specific metaphor first. Use a small signature mark only as the last bit of differentiation.
Verification
- Open several icons at 16px and 32px.
- Check dark and light backend schemes.
- Confirm the metaphor matches the content element title.
- Confirm no solid background rectangle is present.
- Confirm no two icons share identical geometry after removing
<title>.
shadcn Preset Workflow
Use this reference when a TYPO3/Fluid project must follow a shadcn/create preset.
Rule
shadcn presets are build-time design-system inputs. They generate CSS variables, fonts, radius, icon-library settings, component class contracts, and registry metadata. A TYPO3 site should not fetch a preset dynamically at runtime. Generate or inspect the preset in a scratch app, then commit the resulting tokens and translated Fluid component classes.
For EXT:desiderio, the current default reference preset is b4hb38Fyj (radix-mira, olive base, Nunito Sans, Phosphor icons). Keep older committed presets available as scoped token blocks when the Site Setting still exposes them. b6G5977cw is a supported alternate preset (radix-lyra, olive/stone, JetBrains Mono, Tabler metadata, square radius, default-translucent menu, subtle menu accent).
Project CLI Context
When a TYPO3 extension has components.json, run:
npx shadcn@latest info --jsonIf this fails because the project is not a TypeScript app, add the smallest possible tsconfig.json and scratch alias files so the configured aliases resolve. A valid TYPO3-oriented setup can point components, ui, utils, lib, and hooks at a .shadcn/scratch folder while the real runtime stays Fluid/TYPO3.
Use components.json for:
tailwind.css: the Tailwind v4 shadcn source, for example
Resources/Private/Tailwind/desiderio.css.
aliases: paths the CLI can resolve duringinfo,view,docs, and registry work.registries: custom registry namespaces such as
"@desiderio": "Resources/Public/ShadcnRegistry/{name}.json".
Scratch App
From any safe temp directory:
npx shadcn@latest create --template astro --preset <preset-id> --name preset-check --yes --no-monorepo --no-reinstall
cd preset-check
npx shadcn@latest add button card badge alert accordion tabs table input label textarea select separator avatar progress skeleton --yesUse the Astro template when the user wants include-file JavaScript or framework-neutral composition patterns. Use Vite/React only when you need to inspect TSX primitive class contracts directly. shadcn create is an alias for initialization; the public https://ui.shadcn.com/create?item=preview UI is useful for visual preset inspection, not for runtime TYPO3 code.
Inspect:
components.json:style,tailwind.baseColor,iconLibrary,
menuColor, and menuAccent.
src/index.css::root,.dark,@theme inline, imported fonts.src/components/ui/*.tsxor generated Astro examples: primitive class contracts,
data-slot, data-state, data-size, spacing, borders, radius, and typography.
Porting To TYPO3
- Put preset tokens into committed CSS, usually
Resources/Public/Css/shadcn-theme.css.
- Scope alternate presets with
body[data-shadcn-preset="<preset-id>"]and
.dark body[data-shadcn-preset="<preset-id>"].
- Keep Tailwind v4 semantic utilities wired through
Resources/Private/Tailwind/desiderio.css. - Translate React
classNameto Fluidclass. - Preserve
data-slot,data-size,data-state,data-orientation,aria-*, and group variants. - Prefer Fluid components in
Resources/Private/Componentsover repeated class strings in every content element. - Put JavaScript behavior into shared include files registered through TypoScript,
f:asset.script, or TYPO3's AssetCollector. Fluid templates emit data attributes and serialized data; they do not carry inline chart/render scripts.
- Make sure radius is not accidentally overridden. In Desiderio,
desiderio.layout.radius = preset lets the shadcn preset own --radius; values such as none, md, or full intentionally override the preset.
Local Registry
Create a root registry.json when a TYPO3 extension should publish reusable shadcn patterns:
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "desiderio",
"homepage": "https://example.test",
"items": []
}Build it with:
npx shadcn@latest build --output Resources/Public/ShadcnRegistryRegistry item files should package source contracts that another project needs:
- Theme token CSS, Tailwind source CSS, Site Settings, and TypoScript asset includes.
- Shared runtime JavaScript include files.
- Documentation explaining that TYPO3 renders Fluid and shadcn supplies tokens/classes.
Do not publish generated Tailwind output unless consumers cannot build it themselves. Keep registry output under a public path only when components.json points at that public namespace.
Switching Presets
When the user gives another preset id, such as b6G5977cw:
1. Confirm whether the id is already supported in the project. 2. If supported, tell the user to set desiderio.shadcn.preset to that id. 3. If supported and the preset has a new source style, set desiderio.shadcn.style to the matching style value. 4. Set desiderio.layout.radius to preset when the preset should control radius. 5. Keep desiderio.typography.fontSans on preset when the preset should control typography. 6. Flush TYPO3 caches. 7. Check a frontend page in light mode. 8. Check the same frontend page in dark mode. 9. Check the styleguide page if it exists. 10. Check at least one TYPO3 backend layout preview.
When the id is not supported yet:
1. Start from a clean or understood git state. 2. Generate a scratch shadcn project outside the TYPO3 extension. 3. Record style from components.json. 4. Record tailwind.baseColor. 5. Record iconLibrary. 6. Record menuColor. 7. Record menuAccent. 8. Open the generated CSS source. 9. Copy every light :root token. 10. Copy every .dark token. 11. Copy font imports and font-family choices. 12. Add body[data-shadcn-preset="<id>"] to the theme CSS. 13. Add .dark body[data-shadcn-preset="<id>"] to the theme CSS. 14. Map font variables to project variables such as --d-font-sans, --d-font-heading, and --d-font-mono. 15. Add the id to the TYPO3 Site Settings enum. 16. Add a new style enum value when needed. 17. Do not make the new id the global default unless the user asked for that. 18. Add/update documentation screenshots. 19. Update README and project docs. 20. Update tests that assert supported presets. 21. Run the relevant CSS build if classes or Tailwind sources changed. 22. Run npx shadcn@latest info --json. 23. Rebuild the local registry when registry.json changed. 24. Run the project tests. 25. Run git diff --check. 26. Flush TYPO3 caches. 27. Browser-check frontend light mode. 28. Browser-check frontend dark mode. 29. Browser-check TYPO3 backend previews. 30. Commit the resulting token/settings/docs slice.
shadcn/create Left-Nav Values
Do not claim every shadcn/create left-nav value is an independent runtime switch unless the project actually renders a matching Site Setting and CSS/data attribute. In Desiderio, the preset id is the primary supported switch:
- Style: stored as metadata and used when manually porting component class
contracts.
- Base Color, Theme, Chart Color, Heading, Font: supported through committed
preset tokens.
- Radius: supported through preset tokens only when the site radius is
preset; otherwise the site radius intentionally overrides it.
- Icon Library: metadata only. TYPO3 backend icons are committed SVG files.
- Menu and Menu Accent: metadata only unless the project adds explicit Site
Settings and CSS rules for those values.
Do not claim arbitrary preset switching works unless the preset is committed or there is a documented generator that writes the tokens.
Styling Token Contract
Use this reference before creating or modifying content-element templates, element CSS, backend-preview CSS, generated icons, chart scripts, or normalization scripts.
Core Rule
Content elements consume the shadcn design system; they do not define their own theme.
- Put raw preset values only in the committed shadcn theme token source, such as
Resources/Public/Css/shadcn-theme.css.
- Treat
:rootas the light-mode base token set and.darkas the override token set. - Write Fluid templates, CSS, SVG, and JavaScript so switching the shadcn preset or toggling
.dark changes the result through tokens.
- Generated Tailwind CSS may contain compiled values because it is build output. Do not
manually edit generated output to fix source-level styling mistakes.
Allowed In Content Elements
Prefer Tailwind/shadcn utility classes and semantic CSS variables:
- Surfaces:
bg-background,bg-card,bg-popover,bg-muted,bg-accent - Text:
text-foreground,text-card-foreground,text-muted-foreground,
text-primary-foreground
- Controls:
bg-primary,text-primary-foreground,border-border,border-input,
ring-ring
- Charts:
var(--chart-1)throughvar(--chart-5) - TYPO3 preview UI: TYPO3 component variables first, then shadcn tokens; avoid fixed color fallbacks
- Icons: root color
var(--icon-color-primary,currentColor), primary paint as
currentColor, and accent paint as var(--icon-color-accent,currentColor)
- Overlays:
color-mix()with semantic tokens, for example
color-mix(in oklch, var(--foreground) 60%, transparent)
Avoid In Content Elements
Do not add these to ContentBlocks/ContentElements, shared component CSS, preview CSS, icons, or generator templates:
- Hex colors such as
#fff,#000,#ff8700 - Raw
rgb(),rgba(),hsl(),hsla(), oroklch()color literals - Fixed dark-only overlays such as
rgba(0,0,0,.7) - Fixed white text for image/nav overlays when a semantic foreground token can express it
- Shadow fallbacks with raw RGB values when
--shadow-*or--d-shadow-*exists - CSS custom-property fallbacks that resolve to raw colors, for example
var(--primary, #2563eb), outside the committed token source
The practical exception is migration tooling that maps known legacy literals to tokens. Keep those mappings isolated and do not copy the legacy literals into live templates.
Verification
Run a literal scan after styling changes:
rg -n "#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(|hsl\(|oklch\(" \
ContentBlocks/ContentElements \
Resources/Public/Css/content-preview.css \
Resources/Public/Css/components.css \
Resources/Public/Css/desiderio.css \
Resources/Public/Js/charts.js \
Build/Scripts/normalize-content-elements.php \
-g '*.css' -g '*.html' -g '*.js' -g '*.svg' -g '*.php'Run the icon-specific audit when available:
php Build/Scripts/audit-icon-contrast.phpExpected source-level findings:
- HTML entities such as arrows or checkmarks are fine.
- shadcn token files may contain raw
oklch()preset values. - generated Tailwind output may contain compiled values.
- migration maps may contain legacy literals only as search keys.
Any live content-element color literal should be replaced with a semantic token or Tailwind/shadcn utility.
#!/usr/bin/env php
<?php
declare(strict_types=1);
use Symfony\Component\Yaml\Yaml;
$root = $argv[1] ?? getcwd();
$root = rtrim($root, DIRECTORY_SEPARATOR);
$autoloadCandidates = [
$root . '/vendor/autoload.php',
getcwd() . '/vendor/autoload.php',
];
$autoload = null;
foreach ($autoloadCandidates as $candidate) {
if (is_file($candidate)) {
$autoload = $candidate;
break;
}
}
if ($autoload === null) {
fwrite(STDERR, "Missing vendor/autoload.php in {$root} or current working directory\n");
exit(2);
}
require $autoload;
$elementsRoot = $root . '/ContentBlocks/ContentElements';
if (!is_dir($elementsRoot)) {
fwrite(STDERR, "Missing ContentBlocks/ContentElements in {$root}\n");
exit(2);
}
$systemFields = [
'uid' => true,
'pid' => true,
'CType' => true,
'colPos' => true,
'sys_language_uid' => true,
'relations' => true,
'systemProperties' => true,
];
$summary = [
'elements' => 0,
'missingConfig' => 0,
'missingFrontend' => 0,
'missingPreview' => 0,
'missingIcon' => 0,
'missingLabels' => 0,
'templatesWithUndeclaredTopFields' => 0,
'templatesWithUnusedTopFields' => 0,
'templatesWithCollectionIssues' => 0,
'fieldsMissingDefaults' => 0,
'linkModelIssues' => 0,
];
$elements = [];
foreach (glob($elementsRoot . '/*', GLOB_ONLYDIR) ?: [] as $dir) {
$name = basename($dir);
$summary['elements']++;
$configFile = $dir . '/config.yaml';
$frontendFile = $dir . '/templates/frontend.html';
$previewFile = $dir . '/templates/backend-preview.fluid.html';
$iconFile = $dir . '/assets/icon.svg';
$labelsFile = $dir . '/language/labels.xlf';
$entry = [
'name' => $name,
'typeName' => null,
'missing' => [],
'declaredTopFields' => [],
'usedTopFields' => [],
'undeclaredTopFields' => [],
'unusedTopFields' => [],
'fieldsMissingDefaults' => [],
'collectionIssues' => [],
'linkModelIssues' => [],
];
foreach ([
'config' => $configFile,
'frontend' => $frontendFile,
'preview' => $previewFile,
'icon' => $iconFile,
'labels' => $labelsFile,
] as $key => $file) {
if (!is_file($file)) {
$entry['missing'][] = $key;
$summary['missing' . ucfirst($key)] = ($summary['missing' . ucfirst($key)] ?? 0) + 1;
}
}
if (!is_file($configFile) || !is_file($frontendFile)) {
$elements[] = $entry;
continue;
}
$config = Yaml::parseFile($configFile);
$entry['typeName'] = $config['typeName'] ?? null;
$topFields = [];
$fieldTypes = [];
$nestedFields = [];
foreach (($config['fields'] ?? []) as $field) {
if (!isset($field['identifier'])) {
continue;
}
$identifier = (string)$field['identifier'];
$topFields[$identifier] = $field;
$fieldTypes[$identifier] = $field['type'] ?? (($field['useExistingField'] ?? false) ? 'Existing' : null);
if (in_array($field['type'] ?? null, ['Select', 'Checkbox', 'Radio'], true) && !array_key_exists('default', $field)) {
$entry['fieldsMissingDefaults'][] = $identifier;
}
if (($field['type'] ?? null) === 'Textarea' && isLinkLikeListField($identifier)) {
$entry['linkModelIssues'][] = sprintf('Top-level field "%s" is a link-like Textarea; use type: Link plus a separate label field.', $identifier);
}
foreach (($field['fields'] ?? []) as $child) {
if (isset($child['identifier'])) {
$childIdentifier = (string)$child['identifier'];
$nestedFields[$identifier][$childIdentifier] = $child;
if (($child['type'] ?? null) === 'Textarea' && isLinkLikeListField($childIdentifier)) {
$entry['linkModelIssues'][] = sprintf('Collection "%s" child field "%s" is a link-like Textarea; use Link fields with separate labels.', $identifier, $childIdentifier);
}
}
}
}
$template = file_get_contents($frontendFile) ?: '';
$templateForFields = preg_replace('#<(script|style)\b[^>]*>.*?</\1>#is', '', $template) ?? $template;
if (
preg_match('/(?:linkLine|linkParts|\.links\s*->[^\\n]*f:split\s*\([^)]*separator:\s*[\'"]\|[\'"])/i', $templateForFields) === 1
) {
$entry['linkModelIssues'][] = 'Frontend template splits pipe-delimited link values; use TYPO3 Link fields instead.';
}
$fixtureFile = $dir . '/fixture.json';
if (is_file($fixtureFile)) {
$fixture = file_get_contents($fixtureFile) ?: '';
if (preg_match('/\|https?:\/\//', $fixture) === 1) {
$entry['linkModelIssues'][] = 'fixture.json contains pipe-delimited Label|URL values; seed structured link objects or separate fields.';
}
}
$usedTop = [];
preg_match_all('/data\.([A-Za-z_][A-Za-z0-9_]*)/', $templateForFields, $matches);
foreach ($matches[1] as $field) {
$usedTop[$field] = true;
}
preg_match_all('/\{data\s*->\s*f:render\.text\(field:\s*[\'"]([A-Za-z_][A-Za-z0-9_]*)[\'"]/', $templateForFields, $matches);
foreach ($matches[1] as $field) {
$usedTop[$field] = true;
}
preg_match_all('/each="\{data\.([A-Za-z_][A-Za-z0-9_]*)\}"\s+as="([A-Za-z_][A-Za-z0-9_]*)"/', $templateForFields, $loops, PREG_SET_ORDER);
foreach ($loops as $loop) {
[, $field, $variable] = $loop;
$usedTop[$field] = true;
if (($fieldTypes[$field] ?? null) !== 'Collection') {
if (($fieldTypes[$field] ?? null) !== 'File') {
$entry['collectionIssues'][$field][] = 'looped field is not Collection or File';
}
continue;
}
$usedNested = [];
preg_match_all('/' . preg_quote($variable, '/') . '\.([A-Za-z_][A-Za-z0-9_]*)/', $templateForFields, $nestedMatches);
foreach ($nestedMatches[1] as $nestedField) {
$usedNested[$nestedField] = true;
}
preg_match_all('/\{' . preg_quote($variable, '/') . '\s*->\s*f:render\.text\(field:\s*[\'"]([A-Za-z_][A-Za-z0-9_]*)[\'"]/', $templateForFields, $renderMatches);
foreach ($renderMatches[1] as $nestedField) {
$usedNested[$nestedField] = true;
}
$declaredNested = array_keys($nestedFields[$field] ?? []);
$undeclaredNested = array_values(array_diff(array_keys($usedNested), $declaredNested));
$unusedNested = array_values(array_diff($declaredNested, array_keys($usedNested)));
if ($undeclaredNested !== []) {
$entry['collectionIssues'][$field]['undeclaredNestedFields'] = $undeclaredNested;
}
if ($unusedNested !== []) {
$entry['collectionIssues'][$field]['unusedNestedFields'] = $unusedNested;
}
}
$declaredTop = array_keys($topFields);
$entry['declaredTopFields'] = $declaredTop;
$entry['usedTopFields'] = array_keys($usedTop);
$entry['undeclaredTopFields'] = array_values(array_filter(
array_diff(array_keys($usedTop), $declaredTop),
static fn(string $field): bool => !isset($systemFields[$field])
));
$entry['unusedTopFields'] = array_values(array_diff($declaredTop, array_keys($usedTop)));
if ($entry['undeclaredTopFields'] !== []) {
$summary['templatesWithUndeclaredTopFields']++;
}
if ($entry['unusedTopFields'] !== []) {
$summary['templatesWithUnusedTopFields']++;
}
if ($entry['collectionIssues'] !== []) {
$summary['templatesWithCollectionIssues']++;
}
if ($entry['fieldsMissingDefaults'] !== []) {
$summary['fieldsMissingDefaults']++;
}
if ($entry['linkModelIssues'] !== []) {
$summary['linkModelIssues']++;
}
$elements[] = $entry;
}
ksort($summary);
usort($elements, static fn(array $a, array $b): int => $a['name'] <=> $b['name']);
echo json_encode([
'root' => $root,
'summary' => $summary,
'elements' => $elements,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL;
function isLinkLikeListField(string $identifier): bool
{
$normalized = strtolower(str_replace(['-', '_'], '', $identifier));
return in_array($normalized, ['links', 'children', 'pages', 'itemslinks', 'navlinks'], true);
}