The Engine design system. This file ships inside the package, so it arrives in
node_modules/@engineio/ui/AGENTS.md and versions with the code — point at it
rather than copying its contents into a repo, or the copy goes stale.
Authoritative source: docs/brand/ENGINE-DESIGN-SYSTEM.md and
ENGINE-BRAND.md in the engineio/engine repo. Where this file and those
disagree, they win and this is a bug.
@import "tailwindcss";
@import "@engineio/ui/styles";
@source "../../../node_modules/@engineio/ui/dist";The @source line is not optional and its absence is silent. Tailwind 4
does not scan node_modules, so without it the utility classes the components
are written against are never generated. The tokens still land and any class
that also appears in local source still works, so you get a half-styled app
that reads like a component bug. Path is relative to the CSS file declaring it.
import { Button, Card, CardHeader } from "@engineio/ui"
import { Button } from "@engineio/ui/components/ui/button/index.js"
import * as Card from "@engineio/ui/components/ui/card/index.js"
import { EngineWordmark } from "@engineio/ui/components/brand/index.js"Root barrel exports flat prefixed names (CardHeader). Subpath modules export
Root/Header/Content, so import * as Card works. Both /index.js and the
bare subpath resolve.
Alert, Badge, Button, Card, Checkbox, Dialog, Input, Label, Popover, Progress,
RadioGroup, ResponsiveDialog, Select, Separator, Skeleton, Switch, Table, Tabs,
Textarea, Tooltip. Marks: EngineWordmark, EngineIcon.
Not shipped, on purpose: sonner, form, data-table, drawer, resizable,
carousel, chart. Copy from the engine repo if needed. A standalone drawer is
not on that list because ResponsiveDialog covers what one was wanted for.
| Component | Variants |
|---|---|
| Button | default inverse outline ghost secondary link icon destructive success; sizes sm default lg icon icon-sm icon-lg |
| Badge | default secondary outline partner success warning danger destructive |
| Alert | default success warning danger destructive; optional onDismiss |
| Marks | variant="primary" (white) or "secondary" (Off Black) |
One panel, three surfaces. Below 768px it is a bottom sheet; above it, a centred
modal, or an anchored popover with desktop="popover". Reach for it whenever a
dialog has to survive a phone — a centred modal on a 390px viewport is the thing
it exists to stop.
<ResponsiveDialog bind:open>
<ResponsiveDialogTrigger>
{#snippet child({ props })}<Button {...props}>Adjust limits</Button>{/snippet}
</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Adjust limits.</ResponsiveDialogTitle>
<ResponsiveDialogDescription>…</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
…
<ResponsiveDialogFooter>…</ResponsiveDialogFooter>
</ResponsiveDialogContent>
</ResponsiveDialog>| Root prop | |
|---|---|
open |
bindable; survives the swap when the viewport crosses over |
desktop |
dialog (default) or popover — what auto picks above the breakpoint |
mode |
auto (default), or pin to sheet / dialog / popover |
breakpoint |
px, default 768 |
Content takes showClose (defaults on, off for a popover), swipeToClose
(sheet only), overlayClass, and align / side / sideOffset for the
popover. ResponsiveDialogFooter is the part that earns its keep: stacked
full-width actions on the sheet, a right-aligned row everywhere else, from the
same markup.
Two things worth knowing. The sheet and the modal are the same bits-ui Dialog —
focus trap, scroll lock, Escape, outside click — laid out differently; the
popover is a different primitive and is not modal and does not trap
focus, so do not put a destructive confirmation behind desktop="popover".
And crossing the breakpoint with the panel open remounts it, because there is no
honest way to morph a popover into a sheet. Anything mid-edit inside it wants
state that lives above the panel.
There is one chip. Badge absorbed Tag — a soft tinted chip, not a solid
pill. There is no Tag export and no solid badge variant: an opaque fill can
only be correct on one surface, and badge fills are translucent so they read on
the page, on a card and on a table tile alike.
Use these names; never a literal.
colour --color-background #0E0E0E --color-foreground #FFFFFF
--color-primary #FF006A --color-primary-press #D60059
--color-primary-300 #FF5C9B (ink on a magenta tint)
--color-primary-tint-12 / -24
--color-card #161616 --color-popover #1C1C1C
--color-grey-950 … --color-grey-050 (the only greys)
--color-partner-yellow #FFDD00 (reserved, not in use)
status --color-success #00C46A --color-warning #FFB020 --color-danger #FF3B30
each with -foreground (always Off Black) and -tint-12
--color-destructive aliases danger
radii --radius-tag 6 --radius-field 10 --radius-card-inner 10
--radius-media 14 --radius-card 18 --radius-frame 26
--radius-control 999
motion --ease-brand --ease-brand-out --ease-brand-accelerate
140ms controls · 220ms surfaces · 360–640ms reveals
--animate-{overlay,dialog,sheet,popover}-{in,out}
type --font-brand --font-condensed --font-extra-condensed --font-mono
ROLES ONLY — no face ships here. Bind them in your repo (see below)
depth --shadow-panel --shadow-modal (product chrome and modals only)
utility `field` — the shared input skin, incl. the focus ring
- Three brand colours: Off Black, Pure White, Magenta. One accent per surface, never two.
- Status colour is functional.
success/warning/dangerreport state. Never use them as a categorical palette —successfor "slots" because green looked right spends the only signal they carry. - No off-palette colour. Not Tailwind's stock ramps, not a hex literal. CI fails on both.
- Retired and unavailable: Originals Orange
#FF6200, Sportsbook Blue#00CCFF. No accents, no charts, no status. - Ink on any status or accent fill is Off Black. White fails AA on all three.
- Magenta text: never below 15px bold. Use
--color-primary-300on a tint. - No gradients. No light theme. No
dark:variants — the dark palette is the only palette. No drop shadows on brand surfaces. No coloured borders, and no coloured left-edge accent to signal category or ownership. - Radii by role, and the outer frame is always larger than the inner panel.
- Borders are 1px hairline or 1.5px container rule. Nothing else.
- No emoji, anywhere. Only
×for close and✱for footnotes. - Voice: declarative, British/AU spelling, headlines end in a full stop.
- Icons: Lucide, 2px stroke,
currentColor.
Do not fork a component to add a variant, and do not upstream a product-only variant. Every variant map is exported:
import { buttonVariants } from "@engineio/ui"
import { tv } from "tailwind-variants"
export const appButtonVariants = tv({
extend: buttonVariants,
variants: { variant: { drawer: "w-full justify-start rounded-none …" } },
})Brand rules stay in the package; product variants stay in the product. Every
primitive also passes class through cn, so <Button class="w-full" /> works
without !important.
State these rather than working around them silently.
-
No categorical palette. A per-topic hue set is an unmade brand decision.
-
Two deviations from the brand document, both deliberate: Badge is one component where §6 specifies two, and Alert signals state with a coloured left-edge bar which §11 prohibits for category or ownership — state is a narrower reading, not an exemption.
-
No mono face. JetBrains Mono is retired;
--font-monoresolves to the brand face. Set it yourself if a surface genuinely needs character-cell alignment. -
No typeface ships with this package. It names the type roles and binds none of them. Each consumer declares its own
@font-faceblocks, keeps the binaries in its own repo, and overrides the tokens:@import "./fonts.css"; /* your own @font-face blocks */ @import "@engineio/ui/styles"; @theme { --font-brand: "ProximaNova", "Helvetica Neue", Arial, sans-serif; --font-condensed: "ProximaNovaCondensed", Arial, sans-serif; --font-extra-condensed: "ProximaNovaExtraCondensed", Arial, sans-serif; }
This is a required setup step and it fails quietly — exactly like a missing
@sourceline. Skip it and the tokens still resolve, nothing errors, and the app renders in the system stack. Generic-looking type is the symptom.Proxima Nova used to ship here. It is commercially licensed and this package is MIT and public, which is why it no longer does.
-
Dialog, Popover and Tooltip do not animate. They are written against
animate-in/fade-in-0/zoom-in-95/slide-in-from-top-2, which come from thetailwindcss-animateplugin. This package neither ships nor depends on it, so those class names generate no CSS — the panels appear and vanish instantly, and the gallery's "enter over 220ms, exit over 140ms" note is aspirational. The--animate-*tokens ResponsiveDialog uses are the replacement; the three older primitives have not been moved onto them yet. -
Engine Integration's accent is unsettled — build Integration in magenta;
--color-partner-yellowexists but is not in use.