@bento/icon
v0.1.4
Published
Icon component
Readme
Icon
The @bento/icon package provides a way to render SVG-based icons in your
application. This package does not ship any icons but instead allows you to
introduce your own icons.
Installation
npm install --save @bento/iconProps
The @bento/icon package exports the Icon component:
import { Icon } from '@bento/icon';
<Icon icon="icon-name" />The following properties are available to be used on the Icon
component:
| Prop | Type | Required | Description |
|------|------|----------|------------|
| children | ReactNode | No | Optional children elements to be rendered within the Icon component when the icon isn't loaded yet. |
| title | string | No | Screen reader accessible title that explains the illustration.
Introducing this property automatically changes the role attribute from presentation to img. |
| rotate | 90 \| 180 \| 270 | No | Rotate the illustration by 90, 180, or 270 degrees. |
| flip | "horizontal" \| "vertical" | No | Flip the illustration horizontally or vertically. |
| mode | "sprite" \| "svg" | No | The rendering mode when outputting the icon.
Either sprite or svg where svg will return the full SVG element,
and the sprite mode will add the icon to the sprite sheet and reference it. |
| slot | string | No | A named part of a component that can be customized. This is implemented by the consuming component.
The exposed slot names of a component are available in the components documentation. |
| slots | Record<string, object \| Function> | No | An object that contains the customizations for the slots.
The main way you interact with the slot system as a consumer. |
| icon | string | Yes | The name or identifier of the icon to be displayed. |
For all other properties specified on the Icon component, the
component will pass them down to the root SVG element of the component. Which
would be the equivalent of you adding them directly to the child component.
Example
Introducing content
By default, the icon component ships with no icon sets or libraries. You need to introduce the icons to the included store. The icon library exposes the following API methods:
Both methods expect the Icon content to be a valid React SVG element.
If you have SVG content that is not a React SVG element, you can use the
included @bento/svg-parser method to transform a string into
the required React SVG element.
NOTE: If you want to learn more about the store, please refer to the
@bento/create-external-storepackage.
set
The set method allows you to synchronously introduce content to the store.
The method expects an object where the key is the icon’s name, as you would
specify in the icon prop name, and the value is the React SVG element that
should be used to render the icon.
import { set } from '@bento/icon';
set({
'icon-name': <svg />,
'another-icon': <svg />,
'yet-another-icon': <svg />
});If you have SVG content that is not a React SVG element, you can use the
included @bento/svg-parser method to transform a string into
the required React SVG element:
import { parser } from '@bento/svg-parser';
import { set } from '@bento/icon';
set({ 'icon-name': parser(icon) });ondemand
The ondemand method allows you to asynchronously introduce content to the
store. The method expects a function that should return a promise that resolves
to the React SVG element that should be used to render the icon. It receives the
icon’s name, which should be loaded as the first argument.
import { parser } from '@bento/svg-parser';
import { ondemand } from '@bento/icon';
ondemand(async function hosted(name:string) {
const icon:string = await fetch(`{name}.svg`).then((res) => res.text());
return parser(icon);
});You can have multiple ondemand loaders specified in your application, and
they will be called in the order that they are specified. The first loader that
returns a valid React SVG element will be used to render the icon.
If your loader throws an error, the next loader will be called. If all loaders
throw an error or no content is returned, the icon will return a null
response and renders the placeholder content if it's specified.
Accessibility
The icons are rendered with role="presentation" and should be considered
decorative and should not be used to convey information to the user. They
should only be used for visual or branding purposes.
Semantic Icons
Semantic icons convey information to the user rather than just pure decoration. This includes icons without text next to them that are used for interactive elements. To ensure they are accessible to all users, you should always describe the icon.
The title prop is used to describe the icon. This
sets the role="img" attribute on the SVG element and adds a title element
to the SVG element with the contents of the title prop.
import { Icon } from '@bento/icon';
<Icon icon="wand" title="A magic wand" />Performance
When introducing content to the store, you should consider the performance
impact of the content you are introducing. The icons should always be
optimized using the appropriate tools and techniques to reduce the size of the
icons. Tools such as svgo can be used to optimize the SVG content.
The Icon component will render the icon as a full SVG element by default.
import { Icon } from '@bento/icon';
<Icon icon="icon-name" />
<Icon icon="icon-name" />
<Icon icon="icon-name" />When rendering the same icon multiple times, this can lead to a lot of duplication in the DOM. Depending on the amount of icons you are rendering and the size of the icons, this can lead to a lot of bytes being transferred over the network.
<svg>
<path d="...full..path..here" />
</svg>
<svg>
<path d="...so..much..duplication" />
</svg>
<svg>
<path d="...much..more..bytes..here" />
</svg>It's worth noting that there are distinct advantages to rendering the icons as full SVG elements when it comes to server-side rendering.
Sprite
To reduce the amount of duplication in the DOM, you can render the icons as a
sprite. The optional mode prop is used to specify how the icon should be
rendered. Switching the mode to sprite instructs the Icon component to
render a use element that references the icon from a sprite sheet
automatically injected sprite sheet.
import { Icon } from '@bento/icon';
<Icon icon="icon-name" mode="sprite" />
<Icon icon="icon-name" mode="sprite" />
<Icon icon="icon-name" mode="sprite" />Would result in the following DOM:
<svg>
<use xlink:href="#bento-svg-sprite-icon-name" />
</svg>
<svg>
<use xlink:href="#bento-svg-sprite-icon-name" />
</svg>
<svg>
<use xlink:href="#bento-svg-sprite-icon-name" />
</svg>Once your application is hydrated on the client, the icons sprite sheet is generated and injected into the DOM. The browser will trigger a paint event and the icons are displayed. The sprite sheet is generated as follows:
<svg id="bento-svg-sprite" style="display: none;">
<symbol id="bento-svg-sprite-icon-name" data-symbol="icon-name" viewBox="0 0 24 24">
<path d="...massive..long..string..here" />
</symbol>
</svg>The sprite sheet container is only created if it doesn't already exist in the DOM. The icons are added to the sprite sheet as they are requested. If an icon is requested multiple times, it will only be added to the sprite sheet once.
This also means that you can inject the spritesheet into the DOM yourself as part of the SSR response to ensure they are available and displayed before the client-side hydration.
Server side rendering
While the Icon component can render the icon content during the server-side
rendering, the content must be available during the rendering of the icons.
When loading the content Asynchronously using the recommended
ondemand method, the content is not available during the
server-side rendering resulting in an empty response.
import { Icon } from '@bento/icon';
<Icon icon="icon-name" />
// null - The icon doesn't have content, so it can't render anything.Depending on your requirements this might be acceptable. If you want to return content during the server-side rendering, you have the following options:
SVG element as a response
As noted above, the Icon component can only render the icon if the content
is available during the server-side rendering. If you want to render
the icon as part of the SSR response, you should use the set method
to introduce the content to the store.
import { set } from '@bento/icon';
set({ 'icon-name': <svg /> });
<Icon icon="icon-name" />
// <svg>...</svg> - The icon has content that can render the icon.The set method is synchronous. If you are using the ondemand method in your
application, please refer to the Asynchronously section.
Asynchronously
When loading your content asynchronously using the ondemand method, it's
unavailable during the server-side rendering. These icons will default render an
empty (null) response. Once your application is hydrated on the client, the
icons will be available, requested in the ondemand method, and rendered
accordingly.
To avoid the empty response, you can either use
Placeholders as mentioned below or change the mode to
sprite to render the icon as a sprite. When an icon is rendered as a sprite,
it will return an SVG element with a use element that references the icon
from the sprite.
import { Icon } from '@bento/icon';
<Icon icon="icon-name" mode="sprite" />
// <svg><use xlink:href="#icon-name"></use></svg>See sprite for more information.
Placeholders
When the icon content is loading or the requested icon doesn't exist the Icon
component will render the provided children as placeholder. This allows you to
render skeleton loaders, loading spinners, or any other content that should be
displayed while the icon is loading.
import { Icon } from '@bento/icon';
<Icon icon="icon-name">
<svg>...</svg>
</Icon>While it might be tempting to use render props to render the conditionally icon fallback differently on the server and client; this is not recommended for applications that need to hydrate the server-rendered content on the client. This will cause a mismatch between the server and client content, which will result in the client-side hydration failing, forcing a complete re-render of the content.
import { Icon } from '@bento/icon';
//
// ANTI PATTERN causes hydration errors:
//
<Icon icon="icon-name" mode="sprite">
{() => server ? <svg>{icon}</svg> : null }
</Icon>Customization
As the Icon component is a wrapper around the Illustration component, you
can use the Illustration's properties to customize the icon. The Icon
component will pass all properties down to the Illustration component
allowing you to rotate or flip the icon. This is useful when you need
to support RTL languages that requires the icon to be altered.
Slots
The component is created using our @bento/slots package and allows the
assignment of the custom slot property to be used for overrides.
The @bento/icon is registered as BentoIcon and introduces the following
slots:
content: Assigned to theIllustrationcomponent that renders the icon content.
See the @bento/slots package for more information on how to use the slot and
slots properties.
Styling
Once you assign the className property to the component, you take full
responsibility for the styling of the component, and it will remove any
default styling that might be applied as part of this component.
import { Illustration } from '@bento/icon';
<Icon icon="magic-wand" className="my-component" />The following data attributes are introduced as part of the component render state:
| Attribute | Description | Example Values |
| -------------- | ----------------------------------------------------- | ------------------------ |
| data-loading | Indicates the icon is loading | "true" |
| data-icon | The name of the icon that is currently being rendered | "magic-wand" |
| data-mode | The rendering mode of the icon | "sprite" / "svg" |
| data-flip | The icon is flipped horizontally or vertically | "horizontal" / "vertical"|
| data-rotate | The icon is rotated by the specified angle | "90" / "180" / "270" |
These data attributes are introduced on the root element of the component and
can be targeted using your previously provided custom className:
.my-component[data-loading] {
/* The component is loading */
}
.my-component[data-flip] {
/* The component is flipped */
}
.my-component[data-rotate] {
/* The component is rotated */
}
.my-component[data-icon="magic-wand"] {
/* The component is rendering the magic wand icon */
}