In a component library shared by several product teams, which changes are breaking even when no property was removed or renamed, and why?
answer
- the public surface is wider than properties
- what a consumer who passes nothing gets
- hooks consumers style against
- where focus lands, which keys work
- declare what is internal, in writing
basics
~20 sChanging a default, removing a documented styling hook or stable markup part, changing focus order or keyboard behavior, changing a role or accessible name, and changing when an event fires all break consumers without touching a property.
solid answer
~50 sA component's public API is everything a consumer is allowed to rely on, not just its property list. Changing a **default** breaks every call site that never set it. Removing or renaming a documented **styling hook** - a named part or an override token - breaks consumer overrides. Restructuring **markup** the library declared stable breaks consumer style rules and tests. Changing **focus behavior** - where focus goes on open and close, tab order, which key activates - breaks keyboard users and their tests. Changing a **role or accessible name** changes what screen readers announce and breaks tests that query by role. Changing **when an event fires** or what it carries breaks handler logic. The discipline is to declare the public surface explicitly, including what is internal, and treat any change to the declared part as MAJOR.
go deeper
Recall that consumers depend on defaults, styling hooks, keyboard behavior and accessible names, not only on the properties they type. Name at least three such changes.
Explain why each hidden change breaks without any compile error, and how declaring the public surface - including what is internal - decides whether a change is MAJOR.
Show how you would make these changes visible in review: generated API reports, pinned interaction tests, labelled change entries, and generous warnings for widely relied-on internals.
Discuss the trade-off between a wide declared surface, which slows refactoring, and a narrow one, which invites consumers into internals, and how override points resolve it.
## Why the property list is not the whole API Semantic versioning requires a package to **declare a public API** and then bump MAJOR for any backward-incompatible change to it. For a function library, the API is roughly the signatures. For a **component library** - coded UI parts shared by many product teams - consumers depend on far more than the properties they pass. They depend on what happens when they pass *nothing*, on how the rendered output can be styled, on how keyboard and assistive-technology users experience it, and on when callbacks fire. None of those changes produce a compile error, which is exactly why they are dangerous: consumers upgrade blind, and a hidden break surfaces in production. ## The breaking changes that hide In a food-delivery app's shared library: | Change | Example | Who breaks | Why nothing warns them | |---|---|---|---| | **Changed default** | The filter chip group switches from multi-select to single-select by default | Every screen that never set the mode | The call site is unchanged and still valid | | **Removed styling hook** | The documented part name for the restaurant card's image is dropped | Teams that restyled that image | Style overrides fail silently, they do not error | | **Restructured stable markup** | A wrapper is inserted inside the order-status row that was documented as flat | Consumer layout rules and tests | Output renders, just wrongly | | **Changed focus behavior** | The cart drawer now returns focus to the page start instead of the element that opened it | Keyboard and screen-reader users | Nothing fails for mouse users or type checks | | **Changed role or name** | The rating display changes from an image with a name to plain text | Screen-reader users, tests that query by role and name | The visual output is identical | | **Changed event timing** | The search field's change callback now fires on blur, not per keystroke | Live-filtering logic | The callback still exists with the same signature | A newly **required** property and a **narrowed** set of accepted values also break, but at least a type checker may catch those. The table's entries are the ones no tool catches by default. ## Focus and semantics are behavior, not styling The WAI-ARIA Authoring Practices describe keyboard contracts per pattern; for a modal dialog, for example, focus moves inside on open and returns to the invoking element on close. When a library implements such a contract, consumers build on it - their own tests, their support documentation, their users' habits. Changing it is a behavior change to the public API even though the code signature is untouched. The same applies to the role and accessible name a component exposes to the accessibility tree: screen readers on every platform, and tests that find elements by role and name, both depend on them. ## Declared surface versus relied-upon surface There is a tension here. If *everything* observable were public, the library could never refactor its internals. The resolution: 1. **Declare the public surface precisely** - properties with types and defaults, events and their timing, named styling hooks and override tokens, documented structure, keyboard and focus contracts, roles and names. 2. **Declare the rest internal, in writing** - for example, 'markup below the named parts may change in any release'. 3. **Give consumers sanctioned override points** so they have no reason to reach into internals. 4. **Still warn when a widely relied-on internal changes**, because people depend on whatever is observable, declared or not. The version number follows the declared contract; the change entry can be more generous. ## Making hidden changes visible in review - Generate a machine-readable report of every component's properties, types and defaults, commit it, and diff it on each change - a changed default becomes a visible one-line diff. - Keep interaction tests that pin keyboard and focus behavior, so a focus change fails a test instead of shipping. - Require each change entry to be labelled patch, minor or major, and ask one review question: **what does a consumer who passes nothing now get?** The outcome is a library whose version numbers mean what they say, so product teams can take minor and patch upgrades without auditing them.
- Consumers styled an internal wrapper element the library never documented. Is removing it breaking?Under the declared contract, no: undeclared internals may change in any release. In practice, if many teams depend on it, the change entry should call it out and the library should consider a documented hook for what they needed. The durable fix is stating explicitly what is internal and offering sanctioned override points so consumers stop reaching past them.
- Can a deliberate change to a component's size be breaking?Yes. A fix restoring the documented size is a PATCH, but a deliberate change to intrinsic size or spacing can shift consumer layouts - a row that grows taller and pushes content out of a fixed panel. Many teams treat deliberate layout-affecting changes as breaking, or at minimum flag them prominently, because consumers cannot see them coming.
- How do you stop hidden breaks slipping into a MINOR release?Make the hidden surface reviewable: a generated report of properties and defaults diffed on every change, interaction tests that pin keyboard and focus behavior, and a mandatory patch, minor or major label per change entry. A reviewer who sees the diff can classify it; one who sees only implementation code usually cannot.
saying these in an interview costs you the question
- If the property list is unchanged, the release cannot be breaking.
- Changing a default is a MINOR because nobody's code has to change.
- Focus order is an implementation detail that may change in any release.
- Everything in the rendered markup is public API forever.
- A role change only affects screen-reader users, so a PATCH is fine.