This package contains a library of NI-styled web components. Components are built using custom elements and Shadow DOM which are native features in modern browsers.
The library is built on the open source FAST Design System library created by Microsoft. This provides several useful starting points:
- A small, performant custom element base class, FAST Element.
- Infrastructure for design system features like design tokens and theming.
- A library of core components that
- are unopinionated in their style and easily stylable
- adherent to browser standards like accessibility
- while not meeting all of NI's use cases, give us a good starting point and extension capabilities
- offer a promising future roadmap
This video (1 hour but watchable in less time at 2x) and this cheat sheet are great ways to get up to speed with the architecture of FAST in no time. 💨
From the repo root directory:
- Run
npm install - Run
npm run build - Run the different Nimble Components test configurations:
-
To view the components and manually test behaviors in Storybook:
npm run storybookNote: You will need to refresh your browser window to see style changes made in source.
-
To run the unit tests:
npm run test-chrome -w @ni/nimble-components
-
Before building a new component, 3 specification documents need to be created:
- An interaction design (IxD) spec to get agreement on the component's behavior and other core requirements. The spec process is described in the
/specsfolder. - A visual design (ViD) spec to get agreement on the component's appearance, spacing, icons, and tokens. The visual design spec should be created in Figma and linked to the component work item and Storybook Component Status page.
- A technical design spec to get agreement on the component's behavior, API, and high-level implementation. The spec process is described in the
/specsfolder.
-
When creating new components, create the folder structure and decide how to implement the component as described in Develop new components.
-
Create Storybook documentation and tests for the component as described in
@ni-private/storybookCONTRIBUTING. -
Run the Storybook command from the repo root:
npm run storybook.This command also causes
nimble-components(andspright-components) to rebuild whenever a source file is changed so that Storybook can reflect the current state. -
Make functional and style changes to the component.
The storybook will hot reload when you save changes, but the styles will not. On each save that changes
index.tsorstyles.ts, you will need to refresh your browser window to see style changes. -
Create or update tests.
To build and run the tests once, from the
nimbledirectory run:npm run test-chrome -w @ni/nimble-componentsSee Unit tests for additional available commands.
-
Test out the component in each of the 3 major browsers: Chrome, Firefox, and Safari (WebKit). For developers on non-Mac platforms, Safari/WebKit can be tested via the Playwright package:
- To run the unit tests with WebKit, use the command
npm run test-webkit -w @ni/nimble-componentsfrom thenimbledirectory.
- To run the unit tests with WebKit, use the command
-
Create change files for your work by running the following from the
nimbledirectory:npm run change -
Update the Component Status table to reflect the new component state.
If a component is not ready for general use, it should be marked as "incubating" to indicate that status to clients. A component could be in this state if any of the following are true:
- It is still in development.
- It is missing important features like interaction design, visual design, or accessibility.
Incubating contributions may compromise on the above capabilities but they still must abide by other repository requirements. For example:
- Start development with a spec describing the high level plan and what's in or out of scope
- Coding conventions (element naming, linting, code quality)
- Unit and Chromatic test coverage
- Storybook documentation
To mark a component as incubating:
- In the component status table, set its status to
⚠️ - In the component Storybook documentation:
- add a red text banner to the page indicating that the component is not ready for general use
- start the Storybook name with "Incubating/" so that it appears in a separate section of the documentation page
- Add CODEOWNERS from both the contributing team and the Nimble team.
To move a component out of incubating status:
- Have a conversation with the Nimble team to decide if it is sufficiently complete. The requirements listed at the top of this section must be met. Some feature gaps like framework integration may be OK as long as we don't anticipate that filling them would cause major breaking changes.
- Update the markings described above to indicate that it is now ready for general use!
Create a new folder named after your component with some core files:
| File | Description |
|---|---|
| specs/*.md | Contains the original API and implementation specifications for the component. |
| index.ts | Contains the component class definition and registration. All TypeScript logic contained in the component belongs here. |
| styles.ts | Contains the styles relevant to this component. Note: Style property values that can be shared across components belong in theme-provider/design-tokens.ts. |
| template.ts | Contains the template definition for components that don't use a fast-foundation template. |
| types.ts | Contains any enum-like types defined by the component |
| models/ | A folder containing any classes or interfaces that are part of the component API or implementation |
| components/ | A folder containing any components that are used within the component but are not exported as public components themselves. |
| testing/component-name.pageobject.ts | Page object to ease testing of this component. |
| tests/component-name.spec.ts | Unit tests for this component. Covers behaviors added to components on top of existing Foundation behaviors or behavior of new components. |
All components should have an import added to src/all-components.ts so they are available in bundled distribution files.
If Fast Foundation contains a component similar to what you're adding, create a new class that extends the existing component with any Nimble-specific functionality. Do not prefix the new class name with "Nimble"; namespacing is accomplished through imports. Use MyComponent.compose() to add the component to Nimble.
If your component is the canonical representation of the FAST Foundation base class that it extends, then in the argument to compose provide a baseClass value. No two Nimble components should specify the same baseClass value.
Sometimes you may want to extend a FAST component, but need to make changes to their template. If possible, you should submit a PR to FAST to make the necessary changes in their repo. As a last resort, you may instead copy the template over to the Nimble repo, then make your changes. If you do so, you must also copy over the FAST unit tests for the component (making any adjustments to account for your changes to the template). When copying over unit tests:
- Put the FAST tests in a separate file named
<component>.foundation.spec.ts - Update the code to follow NI coding conventions (i.e. linting and formatting)
- Add a comment at the top of the file that links to the original source in FAST
Use the css tagged template helper to style the component according to Nimble guidelines. See leveraging-css.md for (hopefully up-to-date) tips from FAST.
import { Button as FoundationButton } from '@ni/fast-foundation';
const styles = css`
${/* My custom CSS for the nimble fancy button */ ''}
:host {
color: gold;
}
`;
export class Button extends FoundationButton {
// Add new functionality (or leave empty if just restyling the FAST component)
}
const nimbleButton = Button.compose({
baseClass: FoundationButton,
styles
// ...
});If you need to compose multiple elements into a new component, use previously built Nimble elements or basic HTML elements as your template building blocks.
Extend FoundationElement and use a simple, unprefixed name, e.g. QueryBuilder.
Use the html tagged template helper to define your custom template. See Declaring Templates for tips from FAST. Reference other nimble components using import { componentNameTag } ...; instead of hard coding the nimble tag name in templates. This improves the maintainability of the repo because it ensures usages of a component will be updated if it is renamed.
If your new component is unique or complex enough that it can't leverage existing components, you will need to write both the template and the logic yourself.
You should still use fast-element features to make it easier to build and maintain the component. See the FAST documentation on Building Components (particularly Defining Elements and Declaring Templates) to learn the features available to you. You can also look at existing components like the dialog for examples. Feel free to reach out to the Design System team for guidance!
This package follows the NI JavaScript and TypeScript Styleguide with some exceptions listed in Coding Conventions.
Component CSS should follow the patterns described in CSS Guidelines.
It is common in web development to represent variations of control states using css classes. While it is possible to apply custom styles to web components based on user-added CSS classes, i.e. :host(.my-class), it is not allowed in nimble for the following reasons:
- The
classattribute is a user-configured attribute. For native HTML elements it would be surprising if setting a class, i.e.<div class="my-class">, caused the element to have a new style that the user did not define in their stylesheet. However, other attributes are expected to have element defined behavior, i.e.<div hidden>. - Classes set in the
classattribute are not as well-typed across frameworks. Users have to contort a bit to use exported enums for CSS class strings while attributes and attribute values are well-typed in wrappers. - Binding to updates in the
classattribute is more difficult / not an expected pattern. This makes it difficult to forward configured properties to inner elements. Alternatively, binding to attributes and forwarding bound attribute values in templates is a well supported pattern.
-
Do not use attribute names that conflict with native attribute names:
- Avoid any names in the MDN HTML attribute reference list (unless the attribute is trying to match that behavior exactly).
- Do a best effort search in relevant working groups for new attributes that may be coming to avoid, i.e. https://github.com/openui and https://github.com/whatwg.
- Avoid any names that are reserved words in JavaScript.
- Avoid any names that are reserved props in React.
-
Use lower-kebab-case for attributes and enum values that are part of a component's public API.
@attr({ attribute: 'error-text' }) public errorText?: string;
-
For attributes that control the visibility of a part, use either the boolean attribute
<part>-visibleor<part>-hidden, i.e.icon-visibleoricon-hidden.The default configuration should be the most common configuration and the boolean attribute should be added for the less common alternate configuration that differs from the default. An element should NOT implement both
-visibleand-hiddenattributes for a given<part>, only one or the other. -
Use the
appearanceattribute to represent mutually exclusive visual modes of a component that represent large style changes. Likely implemented with an attribute behavior.An
appearance-variantattribute may also be used to represent smaller mutually exclusive variations of an appearance. Likely implemented with CSS attribute selectors.
-
When applicable, the default value for an attribute that is allowed to be unconfigured should be first in the enum object, have a descriptive enum name, such as
default,none, etc, based on the context, and be the enum valueundefined. -
Boolean attributes must always default to
false. Otherwise, the configuration in HTML becomes meaningless, as both<element></element>and<element bool-attr></element>result inbool-attrbeing set totrue. -
States representing the following ideas should use those names:
success,error,warning,information.Avoid shorthands, i.e.
warn,infoand avoid alternatives, i.e.pass,fail,invalid.
With an attribute defined there are several ways to react to updates. To minimize performance overhead, prefer in order (may utilize more that one):
-
Respond to attribute values from css:
:host([my-attribute='some-value']) { /* ... */ }
Using attribute selectors in CSS is particularly useful if there are relatively few spots peppered throughout the file where style should be overridden based on a configured attribute.
-
Respond to attribute values using a behavior:
import { css } from '@ni/fast-element'; css` /* ... */ `.withBehaviors( // ... );
Behaviors are useful when a large block of styles is overridden based on the attibute configuration, i.e. on the order of replacing a large chunk of the stylesheet based on the configuration.
Behaviors should not be used for attributes that change rapidly on a page. Behaviors internally change the stylesheets that are on the page and can trigger expensive style recalculations when stylesheets are added and removed from the page based on the attribute value.
Behaviors are ideal for attributes that are set initially on an element and are not expected to change often / ever during the element lifetime. In these scenarios they actually provide an important performance advantage by eliminating large chunks of unnecessary styles from the page that the browser would need to evaluate.
-
Respond to the value of an attribute programmatically. This may be done by binding to an attribute value or listening to an attribute value change.
This should NOT be done for style purposes and instead rely on CSS attribute selectors or behaviors as previously described.
Some valid use cases are reflecting correct aria values based on the updated attribute or forwarding updates to child components.
Components should be robust to having their properties and attributes configured in invalid ways and should typically not throw exceptions. This matches native element behavior and helps avoid situations where client code must be set component state in a specific order.
Instead of throwing an exceptions, components should ignore invalid state and render in a predictable way. This could mean reverting to a default or empty state. This behavior should be covered by auto tests.
Components can also consider exposing an API that checks the validity of the component configuration. Clients can use this to assert about the validity in their tests and to discover why a component is invalid when debugging. See the nimble-table for an example of this.
It is acceptable to throw exceptions in production code in other situations. For example:
- when a case gets hit that should be impossible, like an invalid enum value.
- from a component method when it shouldn't be called in the component's current state, like
show()on a dialog that is already open.
At a minimum all classes should have a block comment and ultimately all parts of the public API should have a block comment as well.
Accessibility is a requirement for all new components. For the Nimble design system, this means
- Focus states are defined for every element and work on all browsers.
- Colors have sufficient contrast across all themes.
This is a collaborative effort between development and design. Designers will do their due diligence to make sure that designs promote accessiblity, and developers must ensure that each design is implemented and tested across browsers and themes.
Animations can trigger users with vestibular disorders. WCAG provides guidance to disable certain kinds of animations when the prefers-reduced-motion CSS media feature is enabled:
An element which moves into place or changes size while appearing is considered to be animated. An element which appears instantly without transitioning is not using animation. Motion animation does not include changes of color, blurring, or opacity which do not change the perceived size, shape, or position of the element.
Nimble interprets this to mean the following types of animations are permitted with prefers-reduced-motion is enabled:
- Animations which don't involve motion (e.g. fades or color changes)
- Animations which involve motion but don't significantly affect the perceived size, shape, or position of the object. The only approved example of this is animating border thickness; other candidates can be proposed via PR (along with an update to these docs).
- Animations which involved motion but the change in size, shape, or position is synchronized with a user interaction (e.g. a mouse drag to move or resize an object or scrolling through a list).
All other motion animations should either be disabled or replaced with a fade animation when prefers-reduced-motion is enabled. Search this repo for prefers-reduced-motion to find examples of how it's done.
Nimble components should leverage inline svg icons from nimble tokens. The icons are exported from nimble tokens as svg strings similar to the following format:
export const fancy16X16: {
name: 'fancy_16_x_16';
data: string;
} = {
name: 'fancy_16_x_16',
data: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><!-- svg path data --></svg>`
};Use the data property to get the svg string:
import { fancy16X16 } from '@ni/nimble-tokens/dist/icons/js';
const fancyCheckbox = FoundationCheckbox.compose<CheckboxOptions>({
// To populate an existing slot with an svg icon
fancyIndicator: fancy16X16.data
// ...
});The project uses a code generation build script to create a Nimble component for each icon provided by nimble tokens. The script is run as part of the npm run build command, and can be run individually by invoking npm run generate-icons. The generated icon components are not checked into source control, so the icons must be generated before running the TypeScript compilation. The code generation source can be found at nimble-components/build/generate-icons.
Every component should export its custom element tag (e.g. nimble-button) in a constant like this:
export const buttonTag = 'nimble-button';Client code can use this to refer to the component in an HTML template and having a dependency on the export will let a compiled application detect if a tag name changes.
For any custom element definition, extend TypeScript's HTMLElementTagNameMap to register the new element. For example:
declare global {
interface HTMLElementTagNameMap {
// register tag name and type of custom element
'nimble-button': Button;
}
}This enables TypeScript to infer the type of a returned element based on its tag name for DOM methods such as document.createElement() and document.querySelector().
Consider whether or not the delegatesFocus shadow DOM option should be set to true for the component.
Some guidelines to follow when deciding whether or not to set delegatesFocus:
- For a component built on top of a fast-foundation component, check the fast-foundation component's README.md to see if the component was built with the expectation that focus will be delegated.
- Non-interactive elements should keep
delegatesFocuswith the defaultfalsevalue. - Interactive controls that contain no focusable components in the shadow root should keep
delegatesFocuswith the defaultfalsevalue. - Interactive controls that contain focusable components in the shadow root should set
delegatesFocustotrue. - Refer to MDN or this table for more information.
If it is determined that the component should delegate focus, it can be configured as shown below:
const nimbleButton = Button.compose({
// ...
shadowOptions: {
delegatesFocus: true
}
});If delegating focus, you must forward the tabindex attribute to any focusable elements in the shadow DOM. Override the tabIndex property and mark it as an attribute:
export class MyComponent {
...
@attr({ attribute: 'tabindex', converter: nullableNumberConverter })
public override tabIndex!: number;
}Then in the template, bind the focusable elements' tabindex to the host component's property:
html<MyComponent>`
<nimble-button
...
tabindex="${x => x.tabIndex}">
</nimble-button>
// or for an element that isn't focusable by default:
<div
...
tabindex="${x => {
const tabindex = x.tabIndex ?? 0;
return x.disabled ? undefined : `${tabindex}`;
}">
</div>`;Add automated tests for the behaviors implemented above. Search for tabindex in other components for inspiration.
Across supported browsers, perform one-time manual verification of cases including the following:
- Setting a negative
tabindexcauses the control to be skipped when tabbing between elements - Setting a non-negative
tabindexcauses the correct part(s) of the element to receive focus when tabbing between elements - The default
tabIndexproperty value reflects the actual behavior when thetabindexattribute isn't set
TypeScript and the FAST library each offer patterns and/or mechanisms to alter the APIs for a component via a mixin.
FAST provides an applyMixins function (which is just an implementation of the Alternative Pattern described in the Typscript docs) to alter the API of a given component with a set of provided mixin classes. For an example, see how the ToggleButton StartEnd mixin is applied.
Another pattern in use within in Nimble is the Constrained Mixin pattern. An example in Nimble is the FractionalWidth mixin which TableColumnText, for example, ultimately extends. This offers the ability for a mixin to extend the functionality of another concrete type and interface with its implementation.
The 'Constrained Mixin' pattern is used for applying mixins that are defined within Nimble, as they do not fundamentally alter existing types, and the applyMixins FAST method is used for consuming mixins exported from the FAST library.
Unit tests are written using karma and jasmine in files named <component-name>.spec.ts.
The following commands can be run from the nimble directory:
-
npm run test-chrome -w @ni/nimble-components: Runs the test suite in chrome.This command runs headlessly. See Debugging commands if you need to see the browser or set breakpoints while running.
-
npm run test-chrome-debugger -w @ni/nimble-components: When run opens a Chrome window that can be used for interactive debugging. Using dev tools set breakpoints in tests and refresh the page, etc.You can also take the page url and open it in a different browser to test interactively.
-
npm run test-webkit-debugger -w @ni/nimble-components: Similar totest-chrome:debuggerbut for WebKit. Can be run on Windows.
Test utilities located in /src/testing may be used for testing:
- performed inside the
@ni/nimble-componentspackage or - by other packages in the monorepo or users consuming the built package
Test utilties located in /src/utilities/tests are just for tests in the @ni/nimble-components package and are not shared externally.
The jasmine unit tests utilize fixture.ts for component tests. The fixture utility gives tools for managing the component lifecycle. For some usage examples see fixture.spec.ts.
If a test is failing on a specific browser but passing on others, it is possible to temporarily mark it to be skipped for that browser by applying the tag #SkipFirefox, #SkipWebkit, or #SkipChrome to the test name:
// Firefox skipped, see: https://github.com/ni/nimble/issues/####
it('sets title when cell text is ellipsized #SkipFirefox', ...);Before disabling a test, you must have investigated the failure and attempted to find a proper resolution. If you still end up needing to disable it, there must be an issue in this repo tracking the failure, and you must add a comment in the source linking to that issue.
Nimble includes three NI-brand aligned themes (i.e. light, dark, & color).
Most user-visible strings displayed by Nimble components are provided by the client application and are expected to be localized by the application if necessary. However, some strings are built into Nimble components and are provided only in English. An application can provide localized versions of these strings by using design tokens set on label provider elements.
The current label providers:
nimble-label-provider-core: Used for labels for all components without a dedicated label providernimble-label-provider-rich-text: Used for labels for the rich text componentsnimble-label-provider-table: Used for labels for the table (and table sub-components / column types)
The expected format for label token names is:
- element/type(s) to which the token applies, e.g.
number-fieldortable- This may not be an exact element name, if this label applies to multiple elements or will be used in multiple contexts
- component part/category (optional), e.g.
column-header - specific functionality or sub-part, e.g.
decrement - the suffix
label(will be omitted from the label-provider properties/attributes)
Components using localized labels should document them in Storybook. To add a "Localizable Labels" section:
- Their story
Argsshould extendLabelUserArgs - Call
addLabelUseMetadata()and pass their declared metadata object, the applicable label provider tag, and the label tokens that they're using
Component custom element names are specified in index.ts when registering the element. Use the following structure when naming components.
nimble[-category][-variant]-presentation
- All Nimble custom elements are prefixed with
nimble-to avoid name collisions with other component libraries. Applications should choose their own unique prefix if they define their own elements. - category can be used to group similar components together alphabetically. Examples include
iconandtable-column. - variant can be used to distinguish alternate configurations of one presentation. For example,
anchor-,card-,menu-, andtoggle-are all variants of thebuttonpresentation. The primary configuration can omit thevariantsegment (e.g.nimble-button). - presentation describes the visual presentation of the component. For example,
button,tab, ortext-field.
Nimble maps base tokens to theme-aware tokens which are then used to style components. These tokens automatically adjust to the theme set by the theme-provider and relate to specific contexts or components.
To modify the generated tokens, complete these steps:
- Edit the
design-tokens*typescript files insrc/theme-provider/. - Rebuild the generated token files by running the repository's build command,
npm run build. - Test your changes locally and create a PR using the normal process.
Public names for theme-aware tokens are specified in src/theme-provider/design-token-names.ts. Use the following structure when creating new tokens.
[element]-[part]-[interaction_states]-[remaining_states]-[token_type]
- Where element is the type to which the token applies (e.g. 'application', 'body', or 'title-plus-1').
- Where part is the specific part of the element to which the token applies (e.g. 'border', 'background', or shadow).
- Where interaction_states is one or more interaction states (e.g. 'active', 'disabled', 'hover', or 'selected'). Multiple values should be sorted alphabetically.
- Where remaining_states the remaining, non-interaction states (e.g. 'accent', 'primary, or 'large'). Multiple values should be sorted alphabetically.
- Where token_type is the token category (e.g. 'color', 'image', 'font', 'font-color', 'height', 'width', or 'size').
For tokens with multiple sizes, use the following structure for element names. E.g. for title:
| Element name |
|---|
| title-plus-2 |
| title-plus-1 |
| title |
| title-minus-1 |
| title-minus-2 |