The Component API: Inputs, Outputs, and Meaningful Defaults In short: A component's inputs and outputs are a public API. Design them like one — narrow types, honest requirements, defaults that mean something, and outputs named after what happened. When most developers build a component, they start with HTML. They lay it out, style it, then add TypeScript at the end to make the buttons do something. Do it the other way round. Write the contract first. Here is why the order matters so much: you can rewrite a template any time you like, and you can almost never change an input. The markup is yours. The input names belong to every team that binds to them. Spending your design effort on the disposable half is backwards. Let's design the API for a . Lab technicians use it to assign a grade after testing a sample. Rule 1: Kill the type The most common mistake in component design is using primitives for business values. What happens when someone passes ? Or ? Or ? Nothing, until a switch statement silently falls through and the badge renders with no colour. The bug appears three screens away from its cause. Define the actual set of allowed values: Now is a compile error, in the editor, before the code is saved. The type is the documentation, the validation, and the autocomplete list all at once. Two practical notes: Prefer a union of string literals to a TypeScript . Unions are erased at compile time, serialise straight to and from JSON, and read naturally in a template. generates runtime code and is awkward across package boundaries. Use machine-readable values, not display text. , not . Display text needs to change for translation, branding, and copy edits; the contract should not have to change with it. Rule 2: Required versus optional, and defaults that mean something Signal inputs make you state which is which. The test for : is there any value that would be a reasonable guess? For , no — a selector that does not know the current grade cannot render. Make the caller say. For , obviously . And the rule that follows: never leave an optional input as . An default pushes a decision onto every consumer. A real default makes the decision once, in the place that knows the answer. Rule 3: Name outputs after events, not actions A common anti-pattern: A presentational component has no business telling its parent to save anything. It does not know whether there is a database. It should report what the user did and let the parent decide what that means. The practical difference shows up on reuse. makes sense in the lab app, in a read-only audit view that just logs it, and in a Storybook story that does nothing. only makes sense in one of those. Use past tense for things that happened ( , , ) and present tense for things being requested ( , ). Assembling the contract Write the TypeScript first. This file is the design document. With the contract fixed, the template is mechanical: Note the members marked . The style guide asks for that on anything only the template reads — it keeps and out of the component's public surface, so nobody starts calling from a test and depending on it. How this reads at the call site Every mistake here is a compile error, not a runtime surprise: Forget → build fails, because it is required. Pass → build fails, because it is not in the union. Type as anything but in → build fails. "But I thought more inputs made a component more reusable" This is the single most expensive wrong belief in component library work, and it is intuitive: more knobs, more situations covered, more reuse. It even works for a while. The trouble is that inputs multiply rather than add. Start with a card that has four booleans — , , , . That is not four states. It is sixteen. Add a fifth and it is thirty-two. Every one of those is a layout somebody could ask for, and you cannot look at any of them and say "that combination is not supported", because the type system says it is. The consequences arrive in order: 1. The template fills with . Structure now depends on boolean combinations, so grid and flex layouts break in configurations nobody tested. 2. You cannot test it. Nobody writes thirty-two visual regression stories. So most combinations ship unverified. 3. You cannot change it. Any input might be in use somewhere across twenty repositories. Removing one is a major version bump. 4. Nobody can use it correctly. A consumer facing fifteen optional inputs cannot tell which are meant to combine. Here is a real comparison, for a card that needs to support a badge in the header and a button in the footer: Configuration (inputs) Composition (projection) --- --- --- Inputs on the component 9 0 Possible visual states 2⁵ × 3 = 96 Unbounded — but the library owns none of them Stories needed for coverage Impractical 3 (with header, without, with footer) New request: header needs two badges New input, minor release, 20 repos Consumer changes their own template. No release. Who owns the layout inside the header The library team The tea…