Skip to content

Introduction

LO-TEK is a design system that does not require a particular framework. The components are CSS classes and web components, that is, things the browser already understands. Whether your team uses React, Astro, Datastar or nothing at all, it is the same system and the same API.

A design system tied to one framework has to be rewritten when the framework is replaced. The browser’s own APIs are not replaced.

The components are divided in three: those that are only CSS, those that attach behaviour to HTML you write, and those that create their content themselves.

A CSS class on an ordinary HTML element. No JavaScript is loaded.

<button class="lt-button" data-variant="secondary">Save draft</button>

Variants are controlled with data- attributes. This is the largest group, and covers buttons, links, badges, form fields, tables, cards, alerts and typography. What they have in common is that the challenge is how they look, not what they do.

A web component you put your own HTML elements into. It creates no markup itself, but sets the attributes that wire them together.

<lt-field>
<label class="lt-label" for="email" data-required="symbol">Email address</label>
<input class="lt-input" id="email" type="email" value="sam@" />
<p class="lt-help-text">We send the receipt to this address.</p>
<p class="lt-error-text">Enter an email address with an @.</p>
</lt-field>

for and id are in the markup, as HTML requires for the field to have a name without JavaScript. The rest of the wiring, aria-describedby and aria-invalid, the server writes with lt.field(). The component exists for servers that cannot call it, and treats the field as invalid because the error message is visible. The elements stay in plain DOM, so FormData, testing tools and form libraries find them as before.

A web component that creates all its content itself, because there is nothing for the server to send.

<lt-connection-status class="lt-connection-status" data-ignore-morph></lt-connection-status>

The category is an exception with a condition, not a third equal route. It applies only to surfaces that arise from something on the client, and that carry no value into a submission. There is one of them: <lt-connection-status>, where the server by definition cannot report that it is unreachable. In addition there is <lt-toast>, which is an imperative API you call rather than markup you write.

Because the problems are different. A button only needs a style. A form field needs four elements to point at each other, and the markup has to be in the HTML the server sends, or the field does not exist until the script has run. A message that the connection is gone, on the other hand, cannot come from the server that is gone.

The condition that decides where a component belongs:

If it carries a value into a submission, or has content the server must send, it cannot create its own markup.

The split also means you load what the component needs and no more: a button is zero JavaScript.

No frame component creates its own content in the DOM. The markup comes from whoever creates it and stays as it is, and the component only attaches behaviour on top. This holds as much in a React app that renders everything in the browser as in a server-rendered app with hydration, and in a Datastar app where the server sends HTML all the way. The full reasoning, and the answer to when you use the builder function rather than the element, is in Markup and behaviour.

No component uses shadow DOM. All the elements are in plain DOM, and your CSS reaches everywhere.

ComponentCategoryStylesheet
<lt-field>framefield.css
<lt-tabs>frametabs.css
<lt-error-summary>frameerror-summary.css
<lt-popover>framepopover.css
<lt-suggestion>framesuggestion.css
<lt-dialog>framedialog.css
<lt-toast>standalonetoast.css
<lt-session-timeout>framesession-timeout.css
<lt-connection-status>standaloneconnection-status.css

All colours, sizes and font sizes are CSS variables in tokens.css, and work in any CSS:

.my-component {
color: var(--lt-color-accent-fill);
padding: var(--lt-spacing-4);
font-size: var(--lt-font-size-m);
}

The same values exist as TypeScript constants and as a Tailwind theme. See Design tokens.

LO-TEK does not assume Tailwind, but does not get in its way. Let Tailwind handle the layout and LO-TEK the components:

<section class="mx-auto max-w-3xl p-6">
<h2 class="mb-4 text-2xl font-bold">Actions</h2>
<div class="flex flex-wrap gap-3">
<button class="lt-button">Submit application</button>
<button class="lt-button" data-variant="secondary">Save draft</button>
</div>
</section>

If the Tailwind classes should use LO-TEK’s values, import @lo-tek/interfaces/tailwind.css. It is an ordinary CSS file, so the package has no dependency on Tailwind, and your version is the only one involved. See Tailwind.