-
Notifications
You must be signed in to change notification settings - Fork 10
Ten Principles for Great General Purpose Web Components
This page lays out a set of principles for creating general-purpose web components that can be readily adopted in a wide range of sites and application. These principles guide the open source basic-web-components project, but anyone creating components for a wide audience may find these helpful. If you'd like to help make the web a better place, please consider applying these principles to your components.
The core principles are:
- Address a common need.
- Do one job really well.
- Work predictably in a wide variety of circumstances.
- Be useful right out of the box.
- Be composable.
- Be styleable.
- Be extensible.
- Think small.
- Adapt to the user and device.
- Deliver the key benefit to HTML authors, not just coders.
These principles largely enumerate aspects of the standard HTML elements. That set of elements has been tried and tested over many years, and great general-purpose web components should aspire to that level of functionality.
The web component ecosystem is evolving rapidly, and these principles will undoubtedly co-evolve with that ecosystem as we all learn better how to create good building blocks. Today, the initial web component platform contains imperfections that make it quite challenging to meet all of the principles listed above. Cases where this is particularly hard, or the best answer is not yet widely understood, are flagged as implementation challenges below.
The goal of the basic-web-components project is to provide solid components for very common user interface patterns. (That extends to providing components useful as building blocks in implementing such patterns.) Good candidates for new components in this project are user interface patterns that a huge number of sites and apps currently implement by hand today. A pull-to-refresh or infinite scrolling component would be good examples, as those patterns appear everywhere.
General-purpose components help other people create their own finished user interfaces. These are intended to be modified and adapted to many situations. Some ways to consider this point:
- Imagine that the set of approximately 100 standard HTML elements were augmented with an optional library of 100 more elements for everyday use. Is your component a plausible candidate for that set? That’s about how general the component should be.
- A component that is hard-wired to deliver a complete, pre-packaged user experience — entailing minimal or no content from the application developer — is unlikely to be general-purpose.
- A component hard-wired to work with a specific backend service is unlikely to be general-purpose. A component that renders a Facebook Like button may be solving a need that’s ubiquitous and useful, but it still wouldn’t be general.
Web components allow you to break apart complex user interface tasks into meaningful pieces. Old frameworks had limitations that led to monolithic widgets — things that could perform numerous tasks depending on how one adjusted their many customization options. Now that the old limitations are largely gone, monolithic widgets that do many jobs are no longer necessary or desirable. Instead, each web component should do one job really, really well.
Here are some ways to gauge whether your component is focused on a single job:
- You should be able to state the single, core role of the component concisely (ideally, in a single sentence) at the top of the component’s source code. If you find yourself listing out multiple responsibilities which are unique to your component, it’s probably doing more than one thing. You should probably break the component apart into smaller pieces. If your component inherits abilities from base classes, that’s another matter; what’s important is that your component itself contributes a very focused set of functionality.
- It should be meaningful to use your component’s attributes/properties/methods in combination. If your component has two properties that can never be used in combination (“If the foo property is true, then the bar property must be set to false...”), you may need to refactor your component into multiple components that derive from a shared base class. The same thing goes for methods that have restrictions (“You can’t invoke the foo() method on this component if the bar attribute is true”).
As an example, consider the separation of concerns among a small set of components in the basic-web-components collection related to navigating through sequences of child elements in various ways. Such a facility is often provided by a carousel component:
The Basic Web Components implementation of this pattern is called basic-sequence-navigator. Rather than deliver this as a monolithic component that tries to cover every conceivable carousel situation, this component is built up from simpler pieces that each focus on doing one thing really well:
- core-selection is a base class in the Polymer library. Its role is to remember an element that has been selected. Beyond that, it imposes no semantics about selection means. It provides no user interface of its own.
- core-selector is another Polymer class which derives from core-selection. Its primary responsibility is to track which of an element’s children is currently selected. It marks a child as selected, but doesn’t impose a notion of what a selected child element should actually look like — that decision is left open to the author.
- basic-selector extends core-selector to add support for content reprojection (see Be composable.). The basic-selector component actually adopts its content reprojection abilities from a mixin defined by another component, basic-element.
- basic-modes simply ensures that only one of its children is shown at any given time. It inherits most of its behavior from basic-selector. To this, basic-modes adds styling to only show the selected child element. That is, it defines the core notion of selection to mean that a selected child — and only the selection child — is the one that is shown.
- basic-sequence inherits from basic-modes, adding the ability to navigate forward and backward through the set of children with optional visual effects. This component doesn’t actually provide any user interface for navigating forward and backward, because there are many possible ways that can be accomplished.
- basic-sequence-navigator implements just one common user interface pattern for navigating forward and backward through a set of children: a pair of Next and Previous buttons. This component hosts an instance of basic-sequence to handle the navigation transitions.
- basic-slideshow subclasses basic-sequence, and adds the notion of timed navigation to basic-sequence to present a sequence of elements (often images) that automatically advance. This is a completely different way to present a sequence of child images, and is therefore delivered as a separate component.
Note that each class is generally focused on doing one thing really well. This factoring of responsibilities allows developers to pick up and get only the functionality they need.
For example, suppose a developer wants to implement an image carousel that uses small dots at the bottom of the carousel to navigate. They can use the basic-sequence component for this purpose, because that handles navigation (but comes with no navigation UI of its own). All the developer will need to do is dynamically create the appropriate number of dot buttons, and wire them up to the basic-sequence’s selectedIndex property (which basic-sequence actually inherits from core-selector).
If your component only has to do one thing, you’d think that would make it easier to create. But doing one thing really well often turns out quite hard to do, because your component will need to perform that function predictably in a wide variety of circumstances.
The standard HTML elements like p, div, img, strong, etc., are workhorses that can be used in many, many combinations. No matter what you throw at them, they continue to function, and (for the most part) the results are predictable. General purpose web components should be just as flexible and resilient.
Here are some ways your general-purpose component should be accommodating:
- Your component should function in a variety of layouts. Authors may want to set CSS display attribute values like inline (or inline-block), block, or table-cell. They may also want to set position values like relative or absolute. Your component should do something reasonable in these cases; it shouldn’t be usable only in one particular layout.
- Your component should cope with a variety of layouts for the child elements. An author might, for example, want to make a child element expand to fill the interior of your component. Alternatively, the author might want to fix the size of your component, then put an overly-tall set of elements inside it and expect to be able to scroll the contents.
- Your component should allow the set of children to be changed at run-time. This is currently challenging to support; see the Be composable principle.
- If the component supports child elements, it should cope with any type of child element. If your component typically expects
elements as children, it should nevertheless be resilient if a child element is not an image. While you might only envision scenarios for the component that entail images, an author may find a new use for it that involves other types of child elements.
- Your component should also function in the case where the author puts no child elements inside. Even if that means the component cannot be seen, the component shouldn’t crash. It could easily be the case that child elements will dynamically be added later through code.
- Your component should allow properties to be set in any order.
- Your component should take on a reasonable size based on its contents. The built-in HTML elements all auto-size, so good components should too; it shouldn’t be necessary to set an explicit size on the component to get it to look right. It’s easy to create an image carousel element that needs to have its height and width hard-coded in styling, or can only cope with images of a certain fixed size, but that greatly limits the carousel’s utility and appeal.
- Your component should cope with run-time changes in child element size. This is a significant implementation challenge. E.g., a common type of general-purpose component is one that performs a layout task, such as creating a set of stacked columns a la Pinterest. Such components not only need to adjust the layout when the window resizes, but also in response to changes in child element size. This can prove difficult, especially in regards to reprojected content (see “Be composable”).
- Avoid having your component’s code traverse or manipulate the DOM outside the component. These operations can easily conflict with other work the author is doing. The author is not expecting your component to do anything to the outer page, and it can be exasperating for them to track down where the unexpected behavior is coming from. (Exception: there’s a certain kind of component, usually invisible, that exists with the explicit purpose of manipulating another component, often indicated with a “target” attribute.)
This list is somewhat daunting. These points represent an ideal to strive for, rather than stating enforced minimum requirements. A general component that does something useful in the most common cases may still be a good start, as long as it can be gradually extended to cope with unusual cases.
Also see the section on adapting to the user and the device.
A good general component should be useful right away, with zero setup required. To evaluate this criterion, apply the “plain tag test”: If an HTML author does nothing more than types your component’s opening and closing tag, then adds some content text or child elements, will they get something useful?
<!-- Plain tag test: this should do something useful! -->
<my-element>
… text or child elements ...
</my-element>
That plain tag should be sufficient for a general-purpose component.
- Avoid requiring attributes on the opening tag. Your component should provide good default values for all attributes. Exceptions are components with no visual representation that act as data loaders (which require, for example, a URL attribute) or invisible data connectors (to general-purpose storage mechanisms like cookies or localStorage).
- Avoid requiring CSS styling. It can be tricky to strike the right balance; see the discussion on how to be styleable.
- Avoid requiring JavaScript to initialize the component or to perform the component’s most basic function.
- Avoid requiring changes to the component host (e.g., the page) in order to properly host the component.
- Likewise, avoid requiring changes to the child elements to get them to work inside the component.
As a rule of thumb, if an author must follow a set of component-specific instructions to get the component to do anything, the component probably isn’t (yet) general purpose.
Much of the point of a general-purpose web component is to provide a foundation upon which others can build. Authors will use your component to create their own user interfaces, often combining your component with other existing components to create a new component. That’s called composability
.
If you want other people to use your component as a piece in one of their own components, you need to support content reprojection. That means that someone should be able to put a <content>
tag inside your component, and your component should be able to treat that content — the elements distributed to the <content>
node — just as if those elements were direct children of the component.
Example: You create a component <sort-children>
that sorts its child elements by some key. Now imagine an author wants to incorporate your <sort-children>
component into a component of their own, maybe one called <sort-and-filter>
. They write their component like so:
<polymer-element name=”sort-and-filter”>
<template>
<sort-children>
<content></content>
</sort-children>
</template>
<script>
...
</script>
</polymer-element>
What this author wants is to have your component take care of sorting the children, and then they’ll do work to filter that set somehow. The challenge is that, in this example, your <sort-children>
component is always going to see exactly one child: the <content>
element. This author expects that your component will dig into the <content>
to find the elements distributed to that node, and sort those.
This is currently a considerable implementation challenge. Writing code to traverse the content tree is tedious and complex, and feels like its working against the grain of the current design for Shadow DOM tree composition. To make this easier, you can take advantage of a set of methods provided by a mixin defined by the component basic-element. The mixin provides three helper properties that mirror standard ones, but support content redistribution:
- flattenChildren works like the standard children property, returning all children (excluding text nodes) found by expanding any distributed content.
- flattenChildNodes works like the standard childNodes property, returning all child nodes (including text nodes) found by expanding any distributed content.
- flattenTextContent works like the standard textContent property, return the concatenated text of all child nodes found by expanding any distributed content.
The Basic Web Components that process their content take advantage of these helpers, so they're already composable; you can distribute content into those components and get the expected result. E.g., the basic-list-box component lets you manipulate a list of items, including items which have been distributed into content nodes.
A huge advantage of web components over earlier frameworks is a better way to let authors give a component a look and feel that’s consistent with their company’s brand and their application’s visual aesthetic. The mechanisms for styling components are fairly new, so it remains to be seen whether those mechanisms will really measure up to what authors need, but the capabilities do appear promising.
That said, there’s an inherent and unresolved tension in creating styleable general-purpose components. Authors want components that satisfy both of the following conflicting criteria:
- No styling should be required. As noted above, a component should be ready to use out of the box and look fairly good. An author should be able to type in a plain tag and get something simple but attractive. They should not have to apply any styles to that component to get it to appear and function normally.
- The component should come with an absolute minimum degree of styling. The more styles the component has, the harder the author must fight with the component to get it to look appropriate for their application’s own visual design.
This problem will be compounded when a component evolves and acquires more styling. Suppose you give your component a light gray background so that its “out of the box” appearance looks reasonable. An author overrides that background to be red so that it fits in with their red visual theme. Later, you decide that your component requires a border somewhere to clearly delineate its contents from the outer page. Unfortunately, the aforementioned author wasn’t prepared to override this new border. When they pick up a new version of your component, they’ll end up with a red background but a gray border. That may not be what they want. If this happens too often, the author may come to feel that the use of a general-purpose component is not worth the trouble.
The best resolution to this tension is still an open question. For the time being, your best bet is to give your general-purpose component an extremely basic visual appearance.
- Avoid hard-coding font choices.
- Avoid the use of color. Generally avoid hard-coding the background or foreground (text) color. In a component with multiple parts, setting some colors may be necessary in order to delineate those parts. In those cases, try to achieve good results using white, black, and shades of gray. For items that will appear directly on the page background, consider using alpha-blended (RGBA) colors instead of flat white/black/gray; these will work on a wider variety of background colors. When possible, use system or browser colors for highlight or focus effects. (Unfortunately, CSS is often not rich enough to cover for new scenarios faced by many components.)
- Avoid stylistic effects (drop shadows and other gradients) that make a strong visual statement.
- Avoid animations unless they communicate a core part of the component’s function. E.g., if a component’s entire purpose is to enable a expandable/collapsable region, then an animated transition for the expand/collapse operation is reasonable. Even then, the animation shouldn’t draw attention to itself.
Keep in mind that there are two kinds of styling: structural styling, which is necessary for the component to function, and aesthetic styling, which lets the component convey a mood or statement through its appearance. If you’re developing a menu bar component, a fair amount of structural styling is required just to get the menu bar elements into the expected position, deliver pop-up effects, and so on. Employ as much structural styling as you believe is necessary. It’s aesthetic styling that you should keep to a minimum in a general-purpose web component.
A general-purpose component should not only be useful in its own right, but also serve as a foundation other people can build upon and specialize to their own needs. Composability partly addresses this (see above), but another, more complex aspect of extensibility is support for subclassing. A web component author should be able to subclass your component (in Polymer, by use of the “extends” syntax) to create a new, specialized version of your component. The subclass should receive all the benefits of your base component class, as well as offer some interesting new abilities.
Unfortunately, creating subclassable web components is currently an implementation challenge. For one thing, there’s no easy way for a subclass to fill in part of a base class instance — to add some new HTML elements to those already provided by the base class. That issue is currently on hold, and may or may not get fixed. In the meantime, subclasses are generally limited to wrapping or surrounding a base class instance through use of the <shadow>
element.
That said, there are some things you can do to make your component more easily subclassable.
For example, you should expose method hooks which subclasses can override to add or refine behavior. Suppose your component wants to do something in response to a user click/tap. Rather than having your component’s event handler directly perform that work, it may be useful to have that event handler invoke a method (“_handleTap”, say, with an initial underscore to indicate you don’t expect the method to be called from the outside). This allows a subclass to override that method and easily replace or extend the behavior you provided.
It can be hard to know ahead of time what sort of hooks other authors will want your component to provide. Normally, the need for such hooks only becomes apparent when someone (possibly you) first tries to use your component as a base class.
If someone has a user interface situation that can be addressed with your general-purpose component, they shouldn’t have to think twice about adopting it. In particular, they shouldn’t worry too much about the effect your component will have on the size and number of downloaded resources required to get their page up and running.
That means general-purpose components should be lightweight in size:
- Prefer vector image forms (Unicode glyphs likely to be already be installed on the user’s device, SVG images) instead of raster image formats (PNG, JPG).
- Avoid framework libraries (e.g., jQuery) that perform tasks — DOM manipulation, XMLHttpRequests — that can be handled directly by the browser. Web components target modern browsers; such frameworks aren’t necessary to get basic work done. (For one opinion, see Do web component developers still need jQuery?.)
Not only will authors use your general component in a wide variety of circumstances, the author’s end users will be a highly diverse population that interacts with your component on many kinds of devices. To the extent you can, you want to support that range of users and devices.
Traditionally, these two concerns have been covered as different topics. Addressing a range of user abilities is called “accessibility” (or “universal access”), while accommodating device aspects such as screen size is (currently) called “responsive design”. In practice, these topics tend to blend together. For example, doing a great job with keyboard input can be considered an accessibility concern (some people can’t use a mouse or pointing device) or a responsive design concern (taking advantage of a device’s keyboard when one is present is an appropriate means of responding to the device’s characteristics).
The point is that work done in the service of one of these concerns generally benefits the other concern. E.g., supporting a wide range of device screen sizes has the additional benefit of helping desktop users with low vision who increase the browser’s zoom level for better legibility. Taking care to support keyboard tabbing for better screen reader support also helps make a component feel great on a device that has a physical keyboard.
Keep in mind that such work helps all users, not just those with a specific physical handicap or device type. A user who can use a mouse just fine may nevertheless appreciate good keyboard support when they're trying to use their laptop on a bouncy airplane seat-back tray. A user with perfect vision may nevertheless find it helpful to temporarily bump up the text size when projecting an application demonstration to a large audience.
Accessibility and responsive design are topics that are well covered in other areas, and are worth reading up on, but here are just a few ways those concerns apply to web components:
- Use CSS media queries in a component’s
<style>
section to apply styling that depends on the device size. This is something of an implementation challenge at the moment, as media queries can only consider the overall window size, not the size of a specific component. Furthermore, visual designers usually want to have control over the “break points” at which these queries are applied. E.g., they may define a “mobile” display width as being less than a certain number of pixels. Currently there is no standard for where these break points should be set, so different component authors will likely make decisions that differ from what an app’s visual designer might want. - Support keyboard shortcuts when your component has the focus, but avoid wiring up shortcuts that will apply to the entire page. Those could easily conflict with keyboard shortcuts the application author wishes to define themselves. Instead, expose methods that are useful to associate with keyboard shortcuts. The author can set up their own keyboard handling if they want, and will find it helpful to be able to easily wire up keys to the methods you have exposed.
- If your component does respond to keyboard interaction when it has the focus, and does not contain individually focusable elements, consider defining a CSS :focus rule that renders a focus rectangle on the overall component. This is a small implementation challenge, as there is no single CSS rule that says, “Apply a focus rectangle here”.
Lots of authors will want to use general-purpose components through plain HTML markup, without writing JavaScript. The world now has a lot of people who can write HTML but don’t consider themselves to be developers. Moreover, even JavaScript developers appreciate the convenience of being able to quickly wire components together using declarative HTML.
The basic points here are:
- Allow data to be passed into your component via attributes. Basic usage of your component shouldn’t require JavaScript function calls. Among other things, this lets an HTML author wire up your component with declarative data binding; see below.
- Have your component notify the host of changes through plain DOM events. Again, an HTML author can wire up two components by writing a declarative binding to have an event on one component trigger invocation of a method on another component.
- Don't require any JavaScript initialization for basic use. This is part of being useful right out of the box..
Data binding is an easy way for an HTML-level author to wire components together without JavaScript. This is made particularly easy with the Polymer “auto-binding” extension to the <template>
element. This lets an author declaratively bind together the value of attributes on separate components in pure markup:
<template is=”auto-binding”>
<user-selector selection=”{{user}}”></user-selector>
<user-profile user=”{{user}}”></user-profile>
</template>
Here the author has bound the selection attribute of the first component to the user attribute of the second, all without having to write JavaScript.
In these binding scenarios, HTML authors may want to be able to bind to read-only attributes on your component. This is currently an implementation challenge in Polymer, as there is currently no way to define bindable, read-only properties.