Skip to content
omaris

Build

Footguns

The mistakes that look right and aren’t — one list, the same one the CLI writes into AGENTS.md.

<!-- Generated by bun run reference from cli/footguns.js. Edit that file. -->

Every one of these compiles, renders, and is wrong. They are the things a reviewer would catch and a linter cannot, collected so that neither you nor an agent has to learn them by shipping them. The same list goes into the AGENTS.md that bunx omaris init writes.

on-* colour classes do not exist

MD3 muscle memory writes text-on-primary or text-on-surface. The names here are text-primary-foreground, text-surface-foreground, text-primary-container-foreground.

Surface tiers always say container

bg-surface-high is not a class; bg-surface-container-high is. Only surface and surface-variant are tier-less.

accent is a near-grey, not an accent colour

The second accent is tertiary. bg-accent on a call-to-action looks broken. There is no accent-container or muted-container.

Two radius scales

rounded-lg (0.625rem) ≠ rounded-shape-lg (16px). Components use rounded-shape-*; match them. rounded-3xl and larger are Tailwind's stock fixed values and ignore the theme's roundness setting.

Tailwind's stock shadows are wrong here

shadow-md, shadow-lg compile but ignore the theme's shadow colour and look wrong in dark mode. Only shadow-0…shadow-5 are theme-aware.

No duration tokens

Write duration-200, duration-300.

TopAppBar does not stick by default

— pass sticky. It elevates on scroll regardless, which reads as a bug if it isn't stuck. flexible only does something with size="medium" or "large".

AppShellContent defaults to width="full"

— a dashboard goes full-bleed on a wide monitor unless you pass width="lg" or "xl".

AppShellSidebar hides below md

(breakpoint="md"). Give phones a NavigationBar in the footer or a modal drawer, or there is no navigation.

That is the whole rule.

There is no NavigationRail

Use a collapsed NavigationDrawer.

Card defaults to variant="outlined"

A grid of flat "cards" is usually this — pass variant="elevated". A Card with href or onclick becomes interactive on its own; never wrap one in your own <a>.

tailwind-merge only resolves within a group

aspect-video does not cancel a component's built-in height. Where a component ships one it gives you a prop instead (Carousel.height).

One Text variant is the whole type decision

text-title-md carries size, line height, tracking and weight. Adding font-semibold after it means the wrong variant was picked, not the wrong weight.

Icons are yours

omaris bundles none; hand any component an <svg> and it sizes and colours it. Lucide matches the built-in glyphs.

Not every action gets a toast

A toast is for work that finished and left nothing on screen to see — "Copied", "Invite resent", "12 rows archived". If the result is visible there is no toast, and a button that would only announce its own name gets no handler at all. The case that gets this wrong most often is the one that feels most deserving: creating something. Add a product and the product is in the list — the list is the confirmation, and "Product added" on top of it is a second, worse copy of news the screen already broke. Same for a rename (the name changed), a delete (the row went), a toggle (the switch moved), a filter (the results changed), an opened panel, and a sent message. Progress of the press itself is the Button's loading; a field error is the field's supportingText; a message that stays until something changes is an Alert in the page. Batch bulk work into one toast rather than one per row. Count the toasts a screen can raise: one or two is a designed interface; more than four means placeholders are wearing a toast as a costume, and the fix is to implement the action or drop the handler — never to shorten the message. Toasts stack, and eight of them over a phone are the interface.

Numbers are Western digits — always, in every language

ar, ckb and fa default to Eastern-Arabic digits under CLDR, so total.toLocaleString() renders ١٬٢٣٤ the moment the interface is in Arabic, and Intl.DateTimeFormat('ar-IQ') dates read ٤ أيلول. Nobody wants that in a product: prices, counts, IDs, phone numbers and dates are read and typed in 0-9. Pin every formatter with latn() from omaris — n.toLocaleString(undefined, latn()), new Intl.DateTimeFormat(locale, latn({ month: 'short' })). Every formatter inside omaris already does; yours has to. Translate the words, never the digits.