Skip to content
All posts

Design engineering a collaborate popover

Building a scalable component accelerated by AI

Share "My Form"People with access
Shareable link
Anyone with the link can
Share form
Embed
Send form
QR code
MaSaJa

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 original share popover, showing an add-people input, a members list, and a shareable link row
Image-1 displaying full feature

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, a separate view for searching and adding collaborators
Image-2 displaying the second screen

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.

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.

The shareable link row with anyone-with-link access selected
Image-3 displaying the shareable link row

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.

Pencil sketch of the redesigned popover
Image-4 displaying the full feature sketch

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.

Share "My Form"People with access
Shareable link
Anyone with the link can
Share form
Embed
Send form
QR code
MaSaJa

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

  1. Discount Usability: 20 Years — Nielsen Norman Group on lightweight usability methods that catch most issues.

  2. Jakob’s Law of Internet User Experience — Jakob Nielsen on why users prefer interfaces that behave like familiar ones.

  3. Jakob’s Law — A concise visual reference for the principle and its implications for UI design.

  4. Atomic Design — Brad Frost on structuring UI into atoms, molecules, organisms, templates, and pages.