Design engineering a collaborate popover
Building a scalable component accelerated by AI
I’ve designed and built many UIs over the years, so intuition plays a big role in recognizing when an interface can be improved. When something feels off about an interaction, that feeling is compressing experience into a signal before you can put theory behind it. The design vocabulary helps you validate and communicate it, but the noticing comes first.
Still, validated intuition makes for stronger design decisions, and validation doesn’t require a formal study. Lightweight methods like heuristic evaluation and discount usability testing1 surface most issues.
Idea to paper
Let’s look at the example above, a share and collaborate popover. My intuition says the interaction is overly complex and could be simplified. Let’s break it down.

The add people flow
The “Add people” input suggests that clicking on it should let us type and search for users to add. Instead, it navigates to a second screen. This closely resembles a standard input field prevalent across the web, so it should behave like one. This false affordance goes against Jakob’s Law23 and introduces unnecessary cognitive load.

The second screen feels unnecessary, adding an extra step users must process before completing their task. The Edit members button also leads to a settings page where the same actions are available. Since we have a dedicated page for this, we can streamline this process for quicker task completion by combining the primary flow and the second screen into a single combobox component.
The shareable link flow
Much of the same reasoning applies to the shareable link feature. We have three elements: the main dropdown, the results only and edit access dropdown, and the copy link button. We can reduce this and improve the UX by having a switch component that disables the access level menu when toggled off, a url input field, and a copy button.

Sketching the solution
After validating my intuition through first principles, the design direction became clear. The solution removes the second screen and consolidates actions into a single combobox.
Sketching starts the engineering side of design, because you’re physically drawing the feature’s atoms, molecules, and organisms4. This is great because the next step is thinking and planning the component architecture.

What happens next depends on the organization. Some teams translate the sketch into a high fidelity Figma mockup first, to align stakeholders and give developers a precise reference. Others go straight from sketch to code and iterate there. For this demo I’m taking the direct route.
I’m also treating the popover as a standalone component. In a real codebase it would live inside a design system, where the combobox, the select, the switch and the toast all come from shared primitives that already carry their own tokens, states and constraints. That’s a topic worth its own post, so I’m leaving it out here.
Paper to architecture
We’ll use Base UI as our primitives library. It handles the accessibility and interaction patterns: semantics, focus management, keyboard interaction, layering and portals, and ARIA wiring. This lets us focus on the application logic and design rather than reimplementing popover behavior from scratch.
Share Popover
├── Trigger: group avatar
└── Popup
├── Collaborators: combobox
│ ├── Search: text input
│ └── Item: avatar, name, role select
└── Shareable link: section
├── Toggle: switch with label
├── URL: read-only input
│ └── Copy: button with toast confirmation
└── Access level: select, visible while the toggle is on
Component hierarchy tree
Data modeling
A shareable link either exists or it doesn’t, so a discriminated union fits: when enabled is false there’s no url or access level to read, and when it’s true both are required. TypeScript narrows on enabled automatically, so no consumer null checks a url that logically can’t be there.
The same pattern applies to async operations. You can’t access results unless you’ve checked for "success", and you can’t access error unless you’ve checked for "error". The generic lets us reuse this across different search result types.
type ShareableLinkState =
| { enabled: false }
| { enabled: true; url: string; accessLevel: "view" | "edit" };
type SearchState<T> =
| { status: "idle" }
| { status: "loading"; query: string }
| { status: "success"; query: string; results: T[] }
| { status: "error"; query: string; error: Error };
The inline search combobox also needs the standard request patterns: debouncing, request cancellation, caching, and pagination. Libraries like TanStack Query or SWR handle these out of the box, but we’ll skip over them for this demo.
Data mutation
Most of the popover’s mutations can be applied optimistically, since the client already knows the resulting state. The shareable link toggle is the exception: enabling it returns a server generated URL, so the UI has to wait for it (the demo fakes this with a hardcoded URL).
async function handleShareableLinkToggle(enabled: boolean) {
if (!enabled) {
await api.deleteShareableLink(formId);
return setShareableLink({ enabled: false });
}
const url = await api.createShareableLink(formId);
setShareableLink({ enabled: true, url, accessLevel: "view" });
}
Performance
Optimistic updates make mutations feel instant. When a user adds a collaborator, we update the UI immediately and roll back only if the server request fails.
Prefetching on hover reduces perceived latency. Users hover before they click, so we can use that time to start fetching data. By the time they click and the popover opens, the data is already there.
Accessibility
A popover needs to move focus in on open, trap it so Tab and Shift+Tab cycle within the popover rather than escaping to the page, and return focus to the trigger when it closes. Base UI handles all of this out of the box. The combobox adds a layer of complexity, because DOM focus stays in the search input at all times and list items are never focused directly. Arrow keys move a virtual highlight across items using the [data-highlighted] attribute, which is what visually indicates the active item while keeping the input editable.
Architecture to build
Building the component revealed refinements the sketch missed. Trying an idea in code takes minutes, so the rest of this section covers the changes that came out of the build.
Most of them were small. The copy button cross-fades between the copy and check icons with a hint of blur, the share actions scale down slightly on press, and the avatar group’s overflow counter uses tabular numbers so it doesn’t jitter. The section headings also carry 2px of left padding to visually align with the combobox input, the kind of subtle adjustment that’s easy to overlook but improves the overall feel.
None of these were planned. They came out of building, trying things, and keeping what felt right.
The demo below shows the final result, a single screen flow that replaces the original multi step interaction. Try typing an email that isn’t in the list to see the invite flow.
For the copy URL feedback I used Base UI’s anchored toast component, which gives users immediate, contextual confirmation that the link was copied.
This was a high level walkthrough, so it isn’t perfect. There are edge cases I didn’t handle and calls I’d revisit. But it’s a strong starting point, and what stays with me is how quickly it came together.
Every project has its own constraints, and the best process is the one you refine through iteration. The goal isn’t to follow a rigid framework, but to develop an instinct for what to design and what to engineer.
Footnotes
-
Discount Usability: 20 Years — Nielsen Norman Group on lightweight usability methods that catch most issues. ↩
-
Jakob’s Law of Internet User Experience — Jakob Nielsen on why users prefer interfaces that behave like familiar ones. ↩
-
Jakob’s Law — A concise visual reference for the principle and its implications for UI design. ↩
-
Atomic Design — Brad Frost on structuring UI into atoms, molecules, organisms, templates, and pages. ↩