Skip to content

Development notes

When replacing a UI library, protect the contracts pages rely on

How this site uses a shared component layer, Button, and Drawer event adapters to separate page dependencies, compatibility responsibilities, and behavior that still needs hands-on verification when replacing underlying components.

林予晴
Published
  • Component design
  • Astro
  • Starwind UI
  • Refactoring

Replacing a button with one from another component library may leave it looking almost identical. But a page depends on more than appearance: the component may need to render a link, avoid submitting a form by default, or act as a Drawer trigger through data attributes.

If every page needs to know the new package’s internal names, the replacement spreads to every consumer. Replacing the underlying implementation again means checking the same details all over.

In the implementation recorded on October 24, 2026, this site kept a site-owned shared component layer between its pages and Starwind source/runtime, giving pages a fixed entry point to the underlying component library. Drawing on the call sites in use on that date, this note explains what that boundary protects and what it cannot prove on its own. It is not a general procedure for replacing every component.

Pages depend on the site interface; the interface chooses the implementation

This shared component layer exposes the site’s own interface. Pages import components from the site’s ui/button or ui/drawer, rather than depending directly on the Starwind source directory or runtime.

The relationship can be simplified to:

Pages and article layouts
  → The site's shared component layer
    → Local variants, Starwind primitive, and runtime

This does not mean wrapping the same component again at every layer. Some components in the shared component layer compose primitives directly. Others use local variants and still render native HTML. The point is where external dependencies are interpreted, not the number of wrappers.

Be explicit about the contract being preserved, too. Prop names, defaults, slots, native elements, and data-slot may already be used by pages or scripts. Keeping the import name alone is not enough to call a replacement compatible.

Button: the same name does not mean the same interface

This site’s shared Button supports as="button", as="a", and as="summary". The button branch defaults to type="button"; a form that needs to submit must explicitly use type="submit". The link branch renders a real anchor.

It also merges variants with the caller’s class and preserves data-slot. When data-slot identifies a Drawer or Popover trigger/close control, the component adds the corresponding data attributes recognized by the underlying runtime.

This entry point therefore does more than re-export an upstream Button of the same name. Replacing it with a component that only supports button and anchor may lose the summary or overlay usage patterns, even if the default appearance stays the same.

Button appearance and HTML semantics explains how to choose an element. Here, the concern is preserving the choices pages have already made, rather than silently changing them when replacing the underlying library.

Events are contracts, not just names

The article’s table-of-contents script sends drawer:set to the Drawer with { open: false } to request closing. It also listens for drawer:change and drawer:change-complete to schedule updates when the state changes and when closing is complete.

In the compatibility layer described here, those site-side events connect to the Starwind controller. On receiving drawer:set, the layer first checks that detail.open is a boolean, then calls setOpen. The runtime’s openChange and closeComplete become the events the site already listens for, respectively.

The actual responsibilities map as follows:

Site-side dependency What the compatibility layer handles
Emits drawer:set Checks detail.open, then asks the controller to change state
Listens for drawer:change Forwards state information from openChange
Listens for drawer:change-complete Forwards completion information from closeComplete
Event source field Maps the runtime’s event to originalEvent

“Request closing,” “state changed,” and “closing complete” refer to different moments. Similar event names are not a reason to route all three through the same callback and assume the page’s subsequent processing will be unaffected.

This table only describes the adapters listed here. It does not guarantee complete equivalence in bubbling, cancellation, or timing for every event. The compatibility layer also adapts opening and closing intents; cancellation behavior needs to be checked along the relevant path. The mere existence of a CustomEvent does not establish full compatibility.

Style mapping, event adaptation, and interaction testing are separate jobs

Shared colors can keep new components consistent with the site’s surfaces, variants can preserve button appearance, and a compatibility layer can bridge events. None of these automatically proves that focus, Escape, or outside clicks still behave as intended.

For the modal Drawer used by the article’s table of contents, for example, replacing the underlying implementation should still be followed by opening it and checking where focus enters, Tab and Shift + Tab, where focus goes after closing, and how Escape is handled. WAI’s modal dialog pattern describes these as concrete interaction responsibilities. Focus generally returns to the trigger on closing, with exceptions that fit the workflow.[5]

The site’s compatibility layer also has cleanup on pagehide and reinstallation on pageshow. Those branches show that lifecycle paths have been designed; they do not prove that returning to the page leaves controls working without duplicate listeners. That requires the corresponding browser scenario. Reading source alone is not a BFCache test.

No-script behavior needs a separate look. The article body and native links should remain readable, and the table of contents also has a noscript list. This content fallback does not mean interactive runtime components work the same way without JavaScript.

The compatibility layer should explain the differences it handles

Keeping a shared component layer does not mean freezing every old behavior forever. An unreasonable page contract can still be changed, but that should be an explicit interface change, not something hidden inside a package replacement.

I would start with one real call site and list the props, HTML, slots, data attributes, and events it depends on. After replacing the implementation, I would verify output and interaction separately: Button tags and default attributes are no substitute for Drawer focus and closing tests. A working demo page also does not show that every page is wired correctly.

Maintaining locally owned UI components looks at how to maintain differences once source code enters a project. This note adds a question for replacement work: which differences belong at the site’s own entry point, so that every page does not have to learn the underlying implementation again?

This note records the contracts and adapters from the observation period described above. It does not revalidate every overlay, lifecycle path, or third-party version, nor does it measure working hours saved by the replacement. The shared component layer gives differences a traceable location; if it can only be kept working with ever more special cases, the page contract needs to be revisited instead of hiding more cost inside the wrappers.

Sources

[5] https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal — W3C WAI — Dialog (Modal) Pattern