Button
The class goes on an ordinary <button>. No JavaScript is loaded, and it works the same in every framework.
How to use it
Section titled “How to use it”<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/tokens/tokens.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/button/button.css"><!-- Or everything in one file: https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/lotek.css --><button class="lt-button">Send application</button><button class="lt-button" data-variant="secondary">Save draft</button><button class="lt-button" data-variant="ghost">Add attachment</button><button class="lt-button" data-variant="danger">Delete application</button>import "@lo-tek/interfaces/tokens.css"import "@lo-tek/interfaces/button.css"lt is used in two ways, and which one depends on whether you create the element or find it.
If you create the markup, you spread the attributes in. That is the route in React, Astro and anything else that renders:
import { lt } from "@lo-tek/interfaces/react"
<button {...lt.button()}>Send application</button>The import goes to /react here, since the block is JSX. From there the keys are called className and htmlFor. In Astro and anything else without React you import @lo-tek/interfaces and get class and for.
If the element is already there, you set them on it. That is the route when you enhance HTML a server or a template has sent:
import { lt } from "@lo-tek/interfaces"
const button = document.querySelector<HTMLButtonElement>("#send")
lt.setAttributes(button, lt.button({ variant: "secondary" }))lt.button() without an argument gives the primary button, which needs no data-variant.
If the variant comes from an API or a URL, validate it:
import { lt, type ButtonVariant } from "@lo-tek/interfaces"
const variant: ButtonVariant = lt.button.isVariant(response.variant) ? response.variant : "primary"
lt.setAttributes(button, lt.button({ variant }))See Type-safe use.
A reusable button in React.
import { lt } from "@lo-tek/interfaces/react"import type { ButtonVariant } from "@lo-tek/interfaces"import type { ComponentProps } from "react"
type Props = ComponentProps<"button"> & { variant?: ButtonVariant}
export function Button({ variant, className, ...rest }: Props) { const button = lt.button({ variant }) return ( <button {...button} {...rest} className={className ? `${button.className} ${className}` : button.className} /> )}className is merged with the system’s class. If it is only spread in after lt.button(), it replaces lt-button, and the button loses its style without anything telling you.
If the team changes framework, it is this one component that is rewritten, not the design system.
ComponentProps<"button"> includes ref, which since React 19 is an ordinary prop and is passed through {...rest} without forwardRef. ComponentPropsWithRef and ComponentPropsWithoutRef are no longer needed.
data-variant | Used for |
|---|---|
| (omitted) | The main action on the page. Use only one. |
secondary | An alternative action next to the main action |
ghost | Low weight, for toolbars, actions in tables and cards |
danger | Deletes, or cannot be undone |
Disabled
Section titled “Disabled”<button class="lt-button" disabled>Send application</button><button class="lt-button" data-variant="secondary" disabled>Save draft</button><a class="lt-button" role="link" tabindex="0" aria-disabled="true">Go to the overview</a>disabled exists only on <button>. If the button is an <a>, use aria-disabled="true" and remove href, otherwise Enter follows it. Write role="link" and tabindex="0" alongside, so screen readers still find the link and can say that it is unavailable. See Link.
A link that looks like a button
Section titled “A link that looks like a button”<a class="lt-button" href="#">Go to the overview</a><a class="lt-button" data-variant="secondary" href="#">Read the terms</a>If you are navigating the user, the element should be an <a> even if it looks like a button. Then right-click, middle-click and “open in new tab” work.
On a narrow screen
Section titled “On a narrow screen”The button text wraps when it has to. A button with a long label, such as “Send the application for housing benefit for the whole of 2026”, would otherwise become wider than the phone, and then the whole page scrolls sideways.
If you need a full-width button, that is the app’s layout:
.form-button { inline-size: 100%;}Classes you can style
Section titled “Classes you can style”| Class | What it is |
|---|---|
.lt-button | The button. The variant comes from data-variant. |
| Variable | Default |
|---|---|
--lt-button-padding | var(--lt-spacing-2) var(--lt-spacing-4) |
--lt-button-radius | var(--lt-spacing-1) |
--lt-button-border-width | 1px |
--lt-button-font-weight | var(--lt-font-weight-medium) |
--lt-button-line-height | var(--lt-line-height-default) |
:root { --lt-button-radius: 9999px;}An example. A button in a table row needs less space than one standing on its own:
<button class="lt-button">Open the case</button><button class="lt-button row-button" data-variant="secondary">Open</button>.row-button {--lt-button-padding: var(--lt-spacing-1) var(--lt-spacing-3);--lt-button-radius: var(--lt-spacing-4);}The class row-button is yours. The system does not know it, and it works because the variable is inherited down into the button.
The default value follows the --lt-spacing-* scale, so the button stays in step with the rest when you rewrite the scale, step by step. See Customising.
Accessibility
Section titled “Accessibility”<button>performs an action,<a>takes you somewhere else. Choose by what happens, not by how it looks.- Inside a
<form>,type="submit"is the default. If the button should not submit the form, writetype="button". - Let the text describe the action: “Delete user” says more than “OK”.
- Do not use a disabled button as the only explanation of why something cannot be done. Show an error message the user can act on.