Design guide
The decisions every newt/ui component follows: naming, the token layer, states, and why there is no motion.
This is the contract a component is written against. It applies whether the component is being added to the library or composed in your own project — a page built from these rules looks like the rest of the system without anyone having to check.
What newt/ui is
A copy-into-your-project library, not an npm runtime dependency. Components are plain HTML and CSS with optional vanilla JavaScript, plus thin framework wrappers. Everything is built from one token file, so any component matches the surfaces, colours and shapes of the client it is inspired by.
Three forms, one source of truth:
| Form | Where it lives |
|---|---|
| HTML/CSS — the visual spec | packages/cli/registry/html/components/<name>.{css,html,js} |
| React | apps/www/registry/bases/newt/ui/<name>.tsx |
| Vue | apps/vue/registry/bases/newt/ui/<name>/ |
When the React or Vue version disagrees with the HTML, the HTML is right.
Naming
- Classes are prefixed
newt-and follow BEM-lite:.newt-block,.newt-block__element,.newt-block--variant. - File names are kebab-case and match the root class:
status-indicator.css→.newt-status. - A variant modifier names a state or an intent, never an appearance:
--danger,--success,--online. Not--red,--green.
That last rule is what lets the light palette exist. A --red modifier would
have to change when the palette does; a --danger one does not.
The token layer
Never hardcode a colour, radius, font, shadow, easing or duration. If the value you need has no token, add the token — see Theming.
The palette the dark theme is built from:
| Token | Value | Use for |
|---|---|---|
--newt-bg-base |
#1e1f22 |
App background, deepest layer |
--newt-bg-surface |
#2b2d31 |
Sidebars, channel lists |
--newt-bg-elevated |
#313338 |
Cards, main content, modals |
--newt-border |
#3f4147 |
Hairlines, dividers |
--newt-text-primary |
#f2f3f5 |
Headings, primary text |
--newt-text-secondary |
#b5bac1 |
Body text |
--newt-text-muted |
#949ba4 |
Captions, placeholders |
--newt-brand |
#5865f2 |
Primary actions, links-as-buttons |
--newt-online / --idle / --dnd / --offline |
#23a55a / #f0b232 / #f23f42 / #80848e |
Presence states |
--newt-danger |
#da373c |
Destructive actions |
Every token, dark and light, is on the colours page.
Anatomy of a component
In order:
- Markup — semantic HTML, with ARIA roles where the native element does
not imply them:
role="status"for a presence dot,role="dialog"plusaria-modal="true"for a modal. - Styles — scoped under the
.newt-<name>root class, reading only--newt-*tokens. - Behaviour, if any — vanilla JavaScript attached through
data-newt-*attributes, never inline handlers. The framework wrappers are a thin layer over the same behaviour. - States — hover,
focus-visible, active, disabled, and every semantic state the component has, all styled. A state with no resting appearance does not exist.
Logical properties only
No component contains margin-left, left:, ml-2 or text-left. Every
inline direction is logical — margin-inline-start, inset-inline-start,
ms-2, text-start — so a right-to-left tree needs a dir attribute and
nothing else.
The one thing CSS cannot express logically is the sign of a sideways transform,
so tokens.css carries --newt-dir, which is 1 normally and -1 under
[dir="rtl"]:
transform: translateX(calc(var(--newt-dir) * 16px));
Directional glyphs — a reply arrow, a chevron, a send icon — mirror with
rtl:-scale-x-100. A clock or a checkmark does not. See RTL.
No motion
newt/ui ships with no transitions, animations or keyframes. This is a constraint, not an oversight.
- Hover, active and focus are distinguishable by their resting appearance — a filled background, a border, a shape change.
- State is encoded in shape where it can be. The idle crescent and the do-not-disturb bar on the status dot are the pattern: you can tell them apart in a screenshot.
- A “live” indicator — typing, streaming, in progress — uses a static marker, not a pulse or a shimmer.
- If something seems to need motion to be legible, it needs another visual cue instead: an icon, a badge, a label.
Composition
Compose the parts that exist rather than writing a new component in the library’s
style. A message row is MessageGroup with an Avatar, a RoleTag and a
Reaction; a settings panel is Card with Fields. Something genuinely missing
is worth an issue, not a local reimplementation that shares none of the
structure.
See Accessibility for the checklist every component has to pass, and Trademark for what the copy and the assets may say.