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/icon
Props
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:
icon | string | Yes | The name or identifier of the icon to be displayed. |
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. | | | |
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. |
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. | | | |
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
<Source language='tsx' code={ SourceIcon } />
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:
- set - Introduce content synchronously.
- ondemand - Introduce content asynchronously.
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-store package.
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) });
<Source language='tsx' code={ SourceIcon } />
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.
<Source language='tsx' code={ SourceDemand } />
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" />
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" />
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" />
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';
<Icon icon="icon-name" mode="sprite">
{() => server ? <svg>{icon}</svg> : null }
</Icon>
<Source language='tsx' code={ SourceLoading } />
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 the Illustration component 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:
data-loading=true: The Icon component will add a data-loading attribute to
the component’s root element when the icon is loading. This can be used
to style the component while the icon is loading.
data-icon="icon-name": The name of the icon that is currently being rendered.
data-mode="sprite": The component is rendering the icon as a sprite.
data-flip=horizontal|vertical: The component is flipped horizontally or
vertically using the provided flip property.
data-rotate=90|180|270: The component is rotated by the provided angle
using the rotate property.
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] {
}
.my-component[data-flip] {
}
.my-component[data-rotate] {
}
.my-component[data-icon="magic-wand"] {
}