A design system you build and maintain here: the tokens, the sds-
class layer, and the Lit elements. Everything else in this repository comes
from those three. The specimen cards, the Storybook pages, the npm package,
and the guide the claude.ai design agent builds with.
Status: experimental. There is no stable release, registry package, deprecation path or upgrade path yet. A consumer pins a commit or a tag from one of the package mirrors and moves with care. Token, class and component names can still change.
| I want to | Start with |
|---|---|
| use the class layer or the web components | Use it in another project |
| publish a documentation site | docs/guides-theme/example.rst |
| explore the design rules and specimens | docs/design-system/index.rst |
| change this repository | Maintain it here |
| publish package mirrors | MAINTAINERS.md |
sds- is the system's initials. Soul Design System reads the same as the
name it replaced, so no class, token or tag had to move.
CSS first. You link one stylesheet and put classes on markup. That is the whole contract, and it needs no JavaScript:
<link rel="stylesheet" href="soul.css">
<body class="sds-app">
<button class="sds-btn sds-btn--primary">Run the checks</button>
</body>Components where you already run JavaScript. The elements render light
DOM and emit exactly those classes. So components.css stays the single
source of truth, and the two are the same markup:
import '@typo3/soul-frontend';
// <sds-button variant="primary">Run the checks</sds-button>Neither is a fallback for the other. The class layer works without JavaScript, so a server-rendered surface adopts the system without a change to its toolchain. Components add behaviour where a browser is already part of it.
This dresses community projects, not TYPO3 itself. Soul is not the design guide for the TYPO3 backend, and not the design of typo3.org or any other official TYPO3 presence. Those surfaces have their own owners and rules, and nothing here speaks for them. A project on this system is not official, and no surface built with it must imply that it is.
It is for the extensions, tools, services and documentation sites the community builds around TYPO3. Each arrives with its own stylesheet, its own button and its own page layout. A reader who crosses from one to the next starts over every time. One token set, one class vocabulary and one documentation theme mean the second project works the way the first did.
Documentation is the half of that which is easy to miss. docs/ renders
through packages/guides-theme/, so a project's pages come out with the
same navigation, code blocks and search as every other project's. That is
what makes an answer findable across them. To take it is a settings file, a
workflow and the commands between them, printed whole in
docs/guides-theme/.
And it does not stop at documentation. A project also has to present itself: a landing page, a feature or comparison page, a download, the page that says who is behind it. That is usually where a project leaves its documentation theme and hires a look of its own. The two halves then no longer resemble each other.
The screens under stories/pages/ are those surfaces, complete pages, and
a consumer offers them as Starting Points.
What a project sells itself with comes from the same tokens and classes as
what it explains itself with. docs/design-system/screens.rst says which
layouts they stand on.
| Output | Command | What it is |
|---|---|---|
| Storybook | make start |
the documentation surface: guidelines, components with live controls, screens. make test builds it into .out/storybook/, and that build goes out below the site at /storybook/ |
specimens/ |
make cards |
every specimen card and screen, from the stories that compose them |
.out/site/ |
make guides |
the documentation, rendered from docs/ by phpDocumentor Guides through this system's own theme |
.out/acceptance/ |
make guides |
every node the renderer can emit, in a root of its own. The theme's check surface, published never |
packages/frontend/dist/ |
make dist |
the publishable ESM package and its types |
.out/bundle/project/ |
make build |
the design system as the files a Design System artifact keeps, for the design agent on claude.ai |
.out/ is the root for generated output git does not keep: the rendered
site, the built Storybook, the suite's output and the assembled split
packages. make clean removes it whole.
Never edit one by hand. Change a component in
packages/frontend/src/components/, the class layer in
packages/frontend/src/styles/components.css, or a value in
packages/frontend/src/tokens/. Then regenerate.
docs/maintaining/ says how sources become
the artefacts this repository ships, and records the decisions behind those
paths. SKILL.md is the operating instruction to design with the system.
The published design-system and frontend pages put reasons beside the rules.
Docker is the only requirement. No Node version to match, no npm ci,
no playwright install. Every task runs in the container, and there is one
way to run each.
make start # bring the stack up and report what is running
make status # what is running, and where it answers
make # every task, with what it does
make verify # the gate
make test # the suiteThe gate is a sequence of named checks, and the suite a set of test files.
While you work, ask for one of either: make verify ARGS=classes, make test ARGS=tests/select.test.ts. make verify ARGS=--help names the checks.
A partial run says so. Only the whole sequence claims that the system is
consistent.
The suite has two runners. Vitest runs every story as a test of its own,
with axe over it in both themes. It also runs the .test.ts files that
render markup into a frame. Playwright runs the .spec.ts files against a
server: the built Storybook, the rendered site, the drop-in. The same story
run starts from the Storybook sidebar.
make start brings Storybook up and prints its address. It is the one
surface. The guidelines as pages with their specimens at the exact viewport
each declares. Every component with live controls and an a11y panel, and
the whole pages beside them.
The port can change. make start picks a free one and reports it, so
a Storybook you already run elsewhere cannot block this one. make status
reports the same addresses at any point, read out of the running
containers. Host and container get the same number on purpose. Vite's
hot-reload websocket addresses the port Storybook listens on, and a
mismatch kills the reload.
TASKS in scripts/task.ts is the task list, and make prints it.
The frontend is on npm, published from the tag the public mirror carries:
npm install @typo3/soul-frontend litThe package comes from packages/frontend/ and carries the committed
dist/, fonts and brand assets. No build runs on install. The package
entry is dist/index.js, with Lit as a peer dependency.
import '@typo3/soul-frontend'; // every sds- element
import '@typo3/soul-frontend/dist/soul.css'; // tokens + class layerInstall lit beside it. Import the entry and not src/index.ts. The
sources ship along, but they are TypeScript a consumer has to compile, and
the manifest's exports resolve the entry.
One stylesheet, documents included. What a renderer emits with no
class, headings, paragraphs, lists, quotes, tables, code, is in soul.css
with everything else. The layer that owns a bare element sets it. What is a
passage's own is sds-prose's, drawn only where a page carries that class.
The import path is the path in the repository. There are no friendly aliases, on purpose. An alias is a second name for one file, and the two drift, or both work and mean slightly different things. What you read here is what you write there.
Or skip the bundler. Copy dist/ from the frontend mirror somewhere public
and link the drop-in. Copy the directory whole: the stylesheet resolves the
fonts beside itself, and the script resolves the icon sprite inside it.
<script src="/soul/soul-boot.js"></script>
<link rel="stylesheet" href="/soul/soul.css">
<script type="module" src="/soul/soul.js"></script>soul-boot.js is for a page with a mode switch, and it loads before the
stylesheet. soul.js is the drop-in build and carries Lit. The package
entry above leaves Lit external. Do not mix the two JavaScript entries on
one page.
This is not maintainer-only. Import it into your own Design System artifact on claude.ai. The design agent then builds with these tokens, these elements live in their states, these layouts and these guidelines.
make design-sync # build, gate, what will change, and the upload planRun .design-sync/.cache/upload-plan.json # in Claude Code — the Artifact tool executes it
make design-synced # record that the artifact now holds this buildA first import makes its own design system from the type. Let it, and do not adopt an existing artifact. A fresh one starts empty, so the upload is everything in it, and only a system made from the "Design System" type is a target.
Then set its link, once per clone. Without one, every sync makes a second system instead of an update to the first:
make design-project # which design system a sync uploads into, and how to get its link
make design-project ARGS=<url> # set it for this clone
make design-project ARGS=--forget # forget it and the cached state — the next sync is a first importNo npm run uploads. The transport is the Artifact tool bound to your
claude.ai login, and a shell script has none. The scripts own everything
else. make design-plan writes the preflight, the uploads, the file list
and the removals, and the agent executes that instead of its own plan.
A red gate stops make design-sync before any upload. Every fault it
names is invisible in review and wrong in every design after it. It checks
mechanics, not judgement. When make design-status lists changed sections
or layouts, look at them: make baseline before a visual change, make shots && make diff after.
docs/design-system/design-with-claude.rst
is the step-by-step: the first import, the design work, the updates, and
what each symptom means.
| Path | |
|---|---|
packages/frontend/src/ |
the design system. Everything below comes from it |
packages/frontend/src/tokens/*.css |
colour, type, control scale, spacing, radius, motion: the values |
packages/frontend/src/styles/styles.css |
the single entry point: tokens, then the component layer |
packages/frontend/src/styles/components.css |
the sds- class vocabulary every surface uses |
packages/frontend/src/styles/components/prose.css |
sds-prose: the box a passage stands in, and the names a renderer writes for nodes with no element |
packages/frontend/src/styles/_specimen.css |
chrome for the cards only. Not in the styles.css closure, so a rendered design never inherits it |
packages/frontend/src/components/*.ts |
the Lit elements and the template functions they render |
packages/frontend/src/lib/ |
the element base, the icon inliner, the static renderer |
packages/frontend/src/index.ts |
the bundle entry. Its import registers every sds-* element |
stories/ |
the specimen every card and screen comes from, and the components with their controls |
docs/ |
the published documentation: the manual, the guideline pages with their specimens, and the prompts they print |
packages/guides-theme/ |
the Composer package that maps phpDocumentor Guides onto this system, and its acceptance render |
tests/ |
the suite: .test.ts under Vitest, .spec.ts under Playwright |
scripts/ |
the tooling behind the tasks |
.infra/ |
Dockerfile, compose and the entrypoint |
.github/ |
the gate on every push, the site published from main, and the release a tag publishes |
specimens/ |
generated: the cards and the screens, the latter on offer as Starting Points in a consumer |
packages/frontend/fonts/ |
generated from @fontsource, in git because the package publishes it |
packages/frontend/assets/icons/ |
generated from @typo3/icons, in git: the sprite, the lookup, and every file the lookup names |
packages/frontend/dist/ |
generated: the drop-in, in git |
.out/ |
generated: everything git does not keep. The upload payload, the publish root, the built Storybook, the suite's output |
docs/maintaining/ |
how the sources, tasks and generated outputs connect |
MAINTAINERS.md |
package mirror credentials and release operations |
SKILL.md |
the build rules: the operating instruction |
Every card's first line is a @dsCard comment with its group, label,
subtitle and viewport. A screen's is @startingPoint. Those lines are the
contract with the Design System pane, and make verify enforces them. A
screen is its own thumbnail.
Edit packages/frontend/src/components/<name>.ts. Every card comes from
the story that composes it. make cards writes it, and make verify fails
on a stale card or on a card with no story behind it. The next generate
reverts a card edited by hand. A card written by hand is a build failure.
The cards stay static HTML with no custom elements in them. The Design
System pane opens them with styles.css and no JavaScript, so what ships
is the markup the element produces. @lit-labs/ssr does that conversion
in Node.
The repository runs its .ts sources directly. Node strips the types, so
there is no build step for development. make dist exists for the publish.
To add a font family or style, edit the FAMILIES list in
scripts/fonts.ts. An icon arrives with its category, in CATEGORIES in
scripts/icons.ts. Never edit generated output.
The icon's identifier is also its path, by its first segment:
actions-search → src/actions/actions-search.svg in @typo3/icons, and
at the same path under https://cdn.jsdelivr.net/npm/@typo3/icons@5.0.3/
or https://raw.githubusercontent.com/TYPO3/TYPO3.Icons/main/. That is how
a surface pulls one this set does not cover. dist/icons.json in that
package lists every identifier and the deprecated aliases.
THIRD-PARTY.md records the provenance.
A missing icon goes to TYPO3/TYPO3.Icons first. The script fails and does not substitute one from another set.
Before a visual refactor, take make baseline, make the change, then make shots && make diff. Anything that moved, moved on purpose.
MIT, see LICENSE. Icons are MIT and the fonts are SIL OFL 1.1; both are in
THIRD-PARTY.md.