Design rules
The ten rules every heyo-ui component follows, and the components that were deliberately left out.
Loading documentation…
These are the rules every component in the library follows. They are also what a pull request is reviewed against.
Never a raw Tailwind colour inside a component. bg-heyo-base, not bg-white dark:bg-neutral-900.
Tokens resolve both modes with light-dark(); data-mode on any ancestor
switches them. A component never branches on colour mode.
A border changes the box; a ring does not. Control edges are ring-1, and the
colours are translucent so one value is correct on every surface.
The heyo-focus utility. Never hand-rolled, so every focusable element in the
system looks identical.
Default control height 32px, default body text 14px. Hierarchy comes from weight and colour, not size: medium names a surface or a group, normal lives inside one.
It shares the page background and a single hairline separates it. The same rule applies to any pane chrome.
Through cn() (clsx + tailwind-merge), and every component sets a stable
data-slot.
Icon props accept a component, an element, or a node. The internal glyphs are
Tabler paths inlined verbatim, so they sit flush with @tabler/icons-react.
Dropdown and Select default to modal={false} — unlike Base UI — because
a menu is a list of things you might not do, and freezing the page behind one
feels broken. Only Dialog and Sheet lock scroll, because only they demand
an answer.
One Dialog (not dialog + alert dialog + confirm dialog), one bar (Meter,
not meter + progress), one avatar radius. A second component that differs by a
prop is a prop.
Several components are missing on purpose. Read this before proposing one — three of them were built first and then removed.
CodeBlock highlights with
one regular expression per language, which covers the snippets that actually
appear in an interface — a config file, an import block, a curl command — for
about a kilobyte. Real grammars belong to Shiki: pass its markup as
children and the block renders it untouched.Dropdown; a drawer is a phone pattern; a resizable
pane belongs to the application's layout, not to its widget library.Calendar needs "add a day" and
locale names, and Date and Intl.DateTimeFormat do both correctly,
including across DST.DatePicker opens a
calendar rather than parsing text, because 3/4/25 is March for half the
world and April for the other.DataTable filters and
sorts on the client because the 95% case is a page of a few hundred rows you
already have. Every input is also controllable, so the same component works
against a server when it is not.Progress. A task with a percentage is still a measurement.
Meter is that measurement, and one bar that always
means the same thing beats two that look identical.AlertDialog or ConfirmDialog. dismissible={false} covers the
first, and a confirmation is a Dialog with two
buttons in its footer.Almost everything in an application is 14px. Size is not the tool.
Production
acme-api · fra1 · 128 MB
Last deployed 4 minutes ago
text-heyo-default is the thing being acted on. text-heyo-subtle is
context — hints, descriptions, idle navigation items, placeholders.Two tools, and they are not interchangeable.
ScrollArea replaces the browser's scrollbar with
real elements: a 6px overlay thumb that appears while you hover or scroll and
fades out after, plus an optional mask that fades content at whichever edge
still has more behind it. Use it for designed surfaces — panes, cards, log
views.heyo-scrollbar, a plain utility, restyles the native scrollbar instead:
thin, translucent, no track, no buttons. No extra DOM, no JavaScript, and it
survives anything — a <table> wrapper, a dialog body, the sidebar. Every
scrollable surface inside the library uses it.