---
title: "As a standalone frontend"
description: "Two files, and no assumption about what rendered the page."
canonical: index.html
navigation-title: "Frontend"
---

<a id="as-a-standalone-frontend"></a>

# As a standalone frontend

- [Two shapes](#two-shapes)
- [What it needs of a browser](#what-it-needs-of-a-browser)
- [One contract across the layers](#one-contract-across-the-layers)
- [The namespace states ownership](#the-namespace-states-ownership)
- [Where to read on](#where-to-read-on)
- [Non-negotiable](#non-negotiable)

Two files, and no assumption about what rendered the page. Markup from PHP,
Twig, Fluid or a template string uses the class layer with no JavaScript.
The custom elements upgrade that markup where there is behaviour to add.

- [Quick start](quickstart.md)
- [Page layout](layout.md)
- [Stylesheets](stylesheets.md)
- [Components](components/index.md)
  - [Controls](components/controls.md)
  - [Content](components/content.md)
  - [Data & machine output](components/data.md)
  - [Media](components/media.md)
  - [Navigation](components/navigation.md)
  - [Forms](components/forms.md)
  - [Overlays](components/overlays.md)
- [Documents](documents.md)

> [!TIP]
> [Quick start](quickstart.md) puts a working surface on a page from the package or
> the drop-in, before the reference below explains each part.

<a id="two-shapes"></a>

## Two shapes

**The drop-in**

Copy the directory somewhere public and link two files. Lit is in the
bundle, because a drop-in has nothing to share a copy with.

```html
<script src="/soul/soul-boot.js"></script>
<link rel="stylesheet" href="/soul/soul.css">
<script type="module" src="/soul/soul.js"></script>
```

**The package**

ESM with `lit` external, for a page that already has a bundler. A
second copy of Lit gives a consumer a second reactive-element registry,
and an element upgrades under the wrong one.

```bash
npm install @typo3/soul-frontend lit
```

```javascript
import '@typo3/soul-frontend';
import '@typo3/soul-frontend/dist/soul.css';
```

<a id="confval-data-theme"></a>

**`data-theme`**

- Type: "light" | "dark"

Forces a mode on a subtree. Put it on `<html>` for a whole page, so the
browser's own scrollbars and form controls match. Without it, the
reader's system decides, and both modes work: they are one declaration.

<a id="confval-soul-boot-js"></a>

**`soul-boot.js`**

- Type: script
- Required

A line or two, loaded **before** the stylesheet and **not** as a module.
It reads the stored choice and writes `data-theme` before the first
paint. `<sds-theme>` then shows the active side, because it reads the
document.

With no choice it writes nothing, and that absence is the third state.
The page follows the machine, and the switch draws the mark for it. A
concrete mode in its place comes back to the switch as a choice the
reader never made.

Without it a switch still switches, but the next page forgets the choice.
The choice lives under `soul-theme`, which is `sds-theme`'s own
default too. To name another, set `data-key` on the tag and give the
element the same one.

<a id="confval-soul-css"></a>

**`soul.css`**

- Type: stylesheet
- Required

The tokens and the `sds-` class vocabulary, in one file. It has no
scope class: the link *is* the opt-in. That is what lets a bare `<p>` or
`<h2>` take its style without a class from the editor.

<a id="confval-soul-inline-css"></a>

**`soul-inline.css`**

- Type: stylesheet

The same sheet with the two families inside it as data URLs. For a page
that goes out as one file: a report, a review, a page under a host's
content security policy. Paste it into a `<style>`. A `<link>` to it
is a fetch such a host blocks.

Its last two rules stand outside every layer. A host of that kind writes a
reset of its own outside the layers. An unlayered rule beats every layered
one whatever its specificity, so the page stands in the host's face, at
the host's size, in light only. `revert-layer` on the root's
`color-scheme` and on the body's margin, font, colour and background
hands them back to the layers. On a page with no such reset the two rules
change nothing.

Run `soul-finish.js` over the page before it goes out, and link no
script. `soul.js` resolves its icon sprite against its own URL, and a
`<use>` across origins draws nothing. `SKILL.md` carries the whole
recipe.

<a id="what-it-needs-of-a-browser"></a>

## What it needs of a browser

**Chrome and Edge 129, Safari 17.5, Firefox 130**: browsers from autumn 2024
and newer. The floor is not a policy. It is the cost of the features below,
and each of them carries weight:

| Feature | What depends on it |
| --- | --- |
| `light-dark()` | every colour token. Both modes are one declaration, which is why they cannot drift; see [Colours](../design-system/colours.md) |
| the `lh` unit | a glyph centred on the line beside it, at whatever line height that line has |
| `<details name>` | a set of answers where one open closes the last, done by the platform |
| `:dir()` | the glyphs that mean *onward*, turned where the text runs the other way |

> [!WARNING]
> Below the floor a page does not degrade. It renders **unstyled**. An
> unsupported `light-dark()` makes every colour token invalid at once, so
> the page loses its palette, not a feature. A project that must serve older
> browsers must say so before it adopts the system.

The JavaScript targets `es2022`, a lower bar than the CSS, and never the
thing that decides. Nothing here uses a shadow root, a container query or
`@layer`.

<a id="one-contract-across-the-layers"></a>

## One contract across the layers

**Tokens** hold the decisions. Every colour, size, space, radius and
duration has a semantic name, and light and dark sit in one declaration.

**Classes** are the vocabulary. `sds-` names what a thing *is*,
`.sds-card`, `.sds-note--warn`, `.sds-table--compact`, and gives
server-rendered markup the whole visual system without JavaScript.

**Elements** add behaviour. Every one renders **light DOM** and emits exactly
the classes above. So an element upgrades the markup that was already there
instead of a second contract. There is no shadow root anywhere in this
system: `sds-` is it.

The classes came first, because the product that proved the system renders
HTML in PHP without a component runtime. Light DOM lets an element emit the
same class vocabulary a server writes. So `components.css` stays the source
of the pixels, and JavaScript adds behaviour, not a second visual
implementation.

```html
<!-- The same pixels, from either column. -->
<sds-badge tone="ok" label="passed"></sds-badge>
<span class="sds-badge sds-badge--ok">passed</span>
```

Use the element where there is state, behaviour or a decision the markup has
to repeat. Use the class where a server already knows the answer and nothing
on the page changes it.

<a id="the-namespace-states-ownership"></a>

## The namespace states ownership

`sds-` is the system's own prefix. `t3-` implies an official TYPO3
surface, which this community system is not. A block starts with the prefix,
a private part appends a double underscore, a modifier appends a double
hyphen, and transient state uses `.is-*`.

The prefix also keeps common names, `card`, `button`, `badge`, out of
collision with application styles. It gives the gate the boundary a reader
sees: an `sds-` name belongs to the system, a screen's layout classes
belong to that screen.

Only the system declares names in that namespace. If a consumer needs an
`sds-` component or modifier that does not exist, the gap closes here. Then
the element, the class layer, the specimen and the documentation agree on it.
A local invention has no render, no test, and a different spelling at the
next consumer.

> [!IMPORTANT]
> **Web components first.** `<sds-code code-lang="bash">`, never a `div`
> with the classes on it. The classes are the fallback for a surface with no
> JavaScript, not the front door. A page that writes an element's own
> `sds-x__y` names holds a copy of something only the system changes.

<a id="where-to-read-on"></a>

## Where to read on

| Page | What it answers |
| --- | --- |
| [Page layout](layout.md) | the page itself: bar, rail, column, bands, footer, and where the layout sheds as the window narrows |
| [How the stylesheets are written](stylesheets.md) | how to write a stylesheet: the layers, the flow contract, the property sets, and what nesting can do |
| [Components](components/index.md) | every element: what it is for, what it takes, what goes between its tags, and the classes it emits |
| [Documents](documents.md) | the second stylesheet, for prose a renderer produced |

<a id="non-negotiable"></a>

## Non-negotiable

> [!WARNING]
> **Never a colour literal.** Not a hex, not an `rgb()`, not a named
> colour. If nothing fits, the answer is a new token, not a local value.

> [!WARNING]
> **One accent, three places.** `--accent` appears on the active
> navigation item, on a shell prompt and on the wordmark's pipe. A fourth
> use makes the first three mean nothing.

> [!NOTE]
> `SKILL.md` carries the operating rules into the design bundle. This
> section is the interface and the reason for its constraints.
> [Design system](../design-system/index.md) does the same for visual decisions, with the
> rendered evidence beside them.
