BridgeHandbookDraft
For agents
Menu
Draft

Migrating a hand-made control

Seven steps to move a hand-made button, tag or table onto the package.

Why

A hand-made control is one an app draws itself (a raw <button> with its own classes, a home-made search box or tabs, the browser's own confirm box) instead of taking it from @brdgapp/ui. Each copy drifts: its own size, color and hover, and often no focus ring or keyboard support.

On 2 November 2026, Bridge OS's main branch had 128 raw <button> elements in 71 files; its open merge request onto the package brings that to 98 in 53 files. Not every one is a mistake (a whole clickable row can stay a button), but most are hand-made versions of a package control. The same mistakes recur, so these steps apply to every control, not only buttons.

What replaces what

Find the hand-made thing on the left; use the component on the right. The Bridge OS examples are real, from the survey of 2 November 2026.

Hand-madeUseIn Bridge OS
A pill-shaped <button> with its own color, padding and text sizeButtonvariant, sizeActivity feed items' Send and Skip (activity-feed-item.tsx); empty-state pills on the organization and contact pages
An outlined pill link ("View more")Buttonvariant="secondary" with render={<a/>}Organization page's View more (org-network-content.tsx)
A round button with only an iconIconButtonaria-labelClose and more buttons written inline
A colored label in a pill or boxTagcolorStatus labels drawn with hex backgrounds
A rounded <input> with a search iconSearchInputSharing's search (sharing-table.tsx); Network overview's search (connectors-tab.tsx)
Buttons with an underline under the active oneTabsSettings' tabs (settings-content.tsx); the admin embed's code tabs
Chips used to switch what a card showsSegmentedControlInvites' Received / Sent chips (connectors-tab.tsx)
A dropdown of plain buttons, or Radix's DropdownMenuActionMenuContact actions menu (contact-actions-menu.tsx); the app launcher and user menu
The browser's own confirm() boxConfirmDialogSharing's Stop sharing; Network settings' Stop all; Settings' Remove key
A native title tooltipInfoHint, HoverTip and TipTip, HoverTip, InfoHintTags with title text
A grid or <table> with capitals headers and its own rowsDataTableSharing's list (sharing-table.tsx); the admin apps table
A "Load more" link under a listDataTableDataTablePaginationSharing (sharing-table.tsx)
if (loading) return <Skeleton/>, or a hand-drawn empty or error boxDataStateEmpty and error states drawn per page (about six different ways)

How to find them

Search the app's code for these. Each hit is a candidate, not a verdict.

Search forWhat it usually is
<buttonA hand-made button or click area. Most become Button or IconButton; a whole clickable row can stay.
rounded-full bg-[A hand-made pill: almost always a Button or a Tag.
confirm(The browser's confirm box: use ConfirmDialog.
#0038FF #0EA02E #E13535Hex colors: use role tokens, and usually a component that already has them.
title="A native tooltip: use Tip, HoverTip or InfoHint.
@radix-uiRadix parts the package replaced with Base UI (menus, dialogs, tooltips).
components/ui/A local copy of a package component: import it from @brdgapp/ui instead.

The MCP's check_code finds most of these in a file at once (see For agents).

Seven steps

  1. Delete every look class from the old markup. Don't carry over text-[13px], padding, height, radius, color, font-semibold, hover: or transition-*. The component's options own all of that.
  2. className is for layout only. shrink-0, mt-4, w-full, self-start. If the look needs a className, the chosen option is wrong: pick another, or fix the component.
  3. Snap to the nearest size. 13px becomes sm (14px); a 10px control becomes xs (12px). A 1–2px change is the expected cost of joining the system; keeping the old pixel is the mistake.
  4. Drop one-off flourishes. Animated arrows, custom hovers, a one-off accent color: remove them so the control matches its siblings.
  5. The bar for "no equivalent" is high. "It's small", "it's a link", "it's a one-off" all map to an option. A true exception isn't a text or icon control at all (a photo used as a trigger, a chart bar). When in doubt, migrate.
  6. If the system can't do it, fix the system, then migrate. Add the option to the component and its handbook page in the same change. Never work around it at the call site.
  7. Check in the browser, not by eye. Search the file for leftover text-[, px-, bg-, rounded-, hover: on migrated lines, and check one element's computed size and color.

Before and after

Left, copied from Bridge OS; right, the package. Press Tab through them and switch to dark.

A pill button

Activity's Send pill against Button.

Hand-made

Package

Press Tab to move between them, then try dark.

In code:

// Before: hand-made
<button className="rounded-full bg-[#0038FF] px-4 py-1.5 text-[13px] font-semibold text-white hover:bg-[#0036D7]">
  Make intro
</button>

// After: the system decides the look
<Button size="sm">Make intro</Button>

// Moving from Bridge OS: asChild becomes render
<Button asChild><Link href="/settings">Settings</Link></Button>   // before
<Button render={<Link href="/settings" />}>Settings</Button>        // after

Sources

  • Bridge OS components.md → Button migration contract (7 rules), generalised to every component.
  • Plan 03-architecture.md → Radix → Base UI (asChild → render).
  • Counts: git grep '<button' on Bridge OS origin/main (5d9b382) and origin/design-system/adopt-ui (b2adf7d), 2026-11-02.
  • Examples: the Bridge OS consistency survey, 2026-11-02 (all 26 screens).