Icon
Decorative and linked icons with optional background.
On this page
Icon sizes and colors an icon from the icon tokens, with an optional filled background. It wraps whatever you put inside, an inline SVG, an <img> or a glyph, and Icon.a turns it into a link with a hover state.
import { Icon } from '@systhemaui/next'
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-6">
<Icon aria-hidden="true">
<svg viewBox="0 -960 960 960" fill="currentColor" focusable="false">
<path d="m422-232 207-248H469l29-227-185 267h139l-30 208ZM320-80l40-280H160l360-520h80l-40 320h240L400-80h-80Z" />
</svg>
</Icon>
<Icon hasBackground aria-hidden="true">
<svg viewBox="0 -960 960 960" fill="currentColor" focusable="false">
<path d="m422-232 207-248H469l29-227-185 267h139l-30 208ZM320-80l40-280H160l360-520h80l-40 320h240L400-80h-80Z" />
</svg>
</Icon>
</div>
)
}ImportLink to this section
import { Icon } from '@systhemaui/next'In a React app without Next.js, import it from @systhemaui/react. There Icon.a renders a plain anchor.
Variants and tagsLink to this section
Icon renders a <span> by default. It has one shorthand, Icon.a, which renders a link (the same shape as Button.a, Chip.a and MediaWrapper.a). Use as for any other element, for example as="div" or as="svg".
hasBackground adds the icon-has-background class: a filled, bordered tile sized by --icon-has-background-size, with the icon inside at --icon-has-background-icon-size.
import { Icon } from '@systhemaui/next'
const mail =
'M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm320-280L160-640v400h640v-400L480-440Zm0-80 320-200H160l320 200Z'
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-6">
<Icon aria-hidden="true">
<svg viewBox="0 -960 960 960" fill="currentColor" focusable="false">
<path d={mail} />
</svg>
</Icon>
<Icon hasBackground aria-hidden="true">
<svg viewBox="0 -960 960 960" fill="currentColor" focusable="false">
<path d={mail} />
</svg>
</Icon>
<Icon.a href="#variants-and-tags" aria-label="Email us">
<svg viewBox="0 -960 960 960" fill="currentColor" aria-hidden="true" focusable="false">
<path d={mail} />
</svg>
</Icon.a>
<Icon.a href="#variants-and-tags" hasBackground aria-label="Email us">
<svg viewBox="0 -960 960 960" fill="currentColor" aria-hidden="true" focusable="false">
<path d={mail} />
</svg>
</Icon.a>
</div>
)
}Hover the two linked icons: as an <a>, an icon takes the hover colors from the colorSystem.icon.hover.* tokens. Icons that are not links keep a flat look.
ExamplesLink to this section
The SVG as the iconLink to this section
With as="svg" the icon element is the SVG itself, which saves a wrapper.
import { Icon } from '@systhemaui/next'
export default function Demo() {
return (
<div className="flex items-center gap-6">
<Icon as="svg" viewBox="0 -960 960 960" fill="currentColor" aria-hidden="true" focusable="false">
<path d="m233-120 65-281L80-590l288-25 112-265 112 265 288 25-218 189 65 281-247-149-247 149Z" />
</Icon>
</div>
)
}An image as the iconLink to this section
Children are sized to fill the icon with object-fit: contain, so a raster or SVG file works as well as inline markup.
import { Icon } from '@systhemaui/next'
export default function Demo() {
return (
<div className="flex items-center gap-6">
<Icon>
<img src="/demo-assets/logo-mark.svg" alt="Northwind" />
</Icon>
<Icon hasBackground>
<img src="/demo-assets/logo-mark.svg" alt="Northwind" />
</Icon>
</div>
)
}SVG markup from the CMSLink to this section
The Icon block stores an icon as an SVG string. Render it with dangerouslySetInnerHTML, and only for markup you trust.
import { Icon } from '@systhemaui/next'
export function CmsIcon({ svg }: { svg: string }) {
return <Icon hasBackground aria-hidden="true" dangerouslySetInnerHTML={{ __html: svg }} />
}Overriding size and colorLink to this section
Every icon rule is wrapped in :where(), so it has no specificity and any utility class wins without !.
import { Icon } from '@systhemaui/next'
const bolt =
'm422-232 207-248H469l29-227-185 267h139l-30 208ZM320-80l40-280H160l360-520h80l-40 320h240L400-80h-80Z'
export default function Demo() {
return (
<div className="flex items-center gap-6">
<Icon className="size-6" aria-hidden="true">
<svg viewBox="0 -960 960 960" fill="currentColor" focusable="false">
<path d={bolt} />
</svg>
</Icon>
<Icon className="size-12 text-amber-500" aria-hidden="true">
<svg viewBox="0 -960 960 960" fill="currentColor" focusable="false">
<path d={bolt} />
</svg>
</Icon>
</div>
)
}PropsLink to this section
Icon takes the props of the element it renders. Icon.a takes link props.
| Prop | Type | Default | Description |
|---|---|---|---|
as | ElementType | 'span' | |
className | string | - | |
hasBackground | boolean | false | |
children | ReactNode | - | |
…and all <span> attributes |
HTML and CSSLink to this section
Without React, add icon (and icon-has-background) to any element around the SVG:
<span class="icon" aria-hidden="true">
<svg viewBox="0 -960 960 960" fill="currentColor" focusable="false"><path d="…" /></svg>
</span>
<a class="icon icon-has-background" href="mailto:hello@example.com" aria-label="Email us">
<svg viewBox="0 -960 960 960" fill="currentColor" aria-hidden="true" focusable="false">
<path d="…" />
</svg>
</a>The hover rules match .icon:is(a):hover, so only a link icon changes color on hover.
| Class | Styles |
|---|---|
icon | :where(.icon) { display: inline-block; width: var(--icon-size); height: var(--icon-size); font-size: var(--icon-size); line-height: var(--icon-size); color: var(--color-icon-normal-color); }:where(.icon) * { display: inline-block; width: 100%; height: 100%; object-fit: contain; object-position: center; color: currentColor; }+ 4 more rules |
icon-has-background | :where(.icon.icon-has-background) { --icon-shadow-color: var(--color-icon-normal-has-background-shadow); --icon-shadow-x: var(--icon-has-background-shadow-x); --icon-shadow-y: var(--icon-has-background-shadow-y); --icon-shadow-blur: var(--icon-has-background-shadow-blur); --icon-shadow-spread: var(--icon-has-background-shadow-spread); display: inline-grid; place-items: center; border-style: solid; width: var(--icon-has-background-size); height: var(--icon-has-background-size…+ 2 more rules |
Next.jsLink to this section
The @systhemaui/next Icon keeps the React API (Icon, Icon.a and as) but renders Icon.a through the Next-aware LinkHelper instead of a plain <a>. Internal links prefetch and navigate on the client, and hash links smooth-scroll, as with Button.a and Card.link. The social icons of the simple footer use Icon.a, so they get the same behavior.
import { Icon } from '@systhemaui/next'
export function SettingsShortcut() {
return (
<Icon.a href="#cookie-settings" hasBackground aria-label="Cookie settings">
<svg viewBox="0 -960 960 960" fill="currentColor" aria-hidden="true" focusable="false">
<path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z" />
</svg>
</Icon.a>
)
}AccessibilityLink to this section
Iconadds no ARIA of its own. Mark a decorative iconaria-hidden="true", and give a real<svg>focusable="false".- A linked
Icon.ahas no visible text, so it needs anaria-labelthat names the destination. The Icon block derives one from the link when the editor leaves it empty. - Give an icon one name only: an
aria-labelon the link, oralttext on an<img>inside it, not both.
See Icons and icon-only controls.