npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@editora/icons

v0.1.12

Published

Framework-agnostic SVG icon library for the Editora UI ecosystem with outline, solid, and duotone variants

Readme

Features

  • 504 icons that share one 24 × 24 grid and one line weight, in categories from arrows and files to social logos and charts, and 1139 aliases for the names you would guess (tick and done are check).
  • Plain strings. renderIconSvg returns an <svg> string, so it works in the browser, in Node (the server, a build script, an email template) and in any framework. iconToDataUri gives the same icon for an <img> or a CSS background.
  • Outline, solid and duotone, three weights, any size and colour, rotate, flip and right-to-left mirroring. Strokes stay crisp: they are the same width in pixels at every size.
  • Accessible by default. An icon is hidden from screen readers unless you give it a title or a label.
  • Extensible. Register your own icons, and they work everywhere the built-in ones do.
  • Written in TypeScript, with no dependencies. The whole catalog is about 48 KB gzipped.
  • For React, use @editora/react-icons, which is built on this package.

Install

npm install @editora/icons

Quick start

A page with no build step loads the module from a CDN:

<style>
  .toolbar { display: flex; gap: 6px; }
  .toolbar button { display: grid; place-items: center; width: 36px; height: 36px; margin: 0; padding: 0; border: 1px solid #d0d7de; border-radius: 8px; background: #fff; color: #24292f; cursor: pointer; }
  .toolbar button:hover { background: #f3f4f6; }
</style>

<div class="toolbar" id="toolbar"></div>

<script type="module">
  import { renderIconSvg } from 'https://cdn.jsdelivr.net/npm/@editora/icons/dist/index.esm.js';

  const actions = { bold: 'Bold', italic: 'Italic', underline: 'Underline', link: 'Insert link', image: 'Insert image', undo: 'Undo' };

  // The button is labelled, so the icon inside it is decorative (the default when it has no title).
  document.getElementById('toolbar').innerHTML = Object.entries(actions)
    .map(([icon, label]) => `<button type="button" aria-label="${label}">${renderIconSvg(icon, { size: 18 })}</button>`)
    .join('');
</script>

With a bundler, import { renderIconSvg } from '@editora/icons' does the same. Icons are drawn 15 pixels wide unless you ask for a size, and they take the text colour of the element they are in (currentColor), so they follow the button's hover and dark-mode colours without any extra work.

Find an icon

listIcons() has every name, hasIcon(name) tells you whether one exists and listIconAliases() maps each alias to its icon. A name is matched without regard to case, and spaces and underscores count as dashes: Chart Line, chart_line and chart-line are the same icon. renderIconSvg returns an empty string for a name it does not know, so a typo draws nothing rather than throwing.

import { listIconAliases, listIcons, renderIconSvg } from '@editora/icons';

const names = listIcons();

// Every alias points at an icon: 'tick' and 'done' are other names for 'check'.
const aliasesOf = new Map<string, string[]>();
for (const [alias, target] of Object.entries(listIconAliases())) {
  aliasesOf.set(target, [...(aliasesOf.get(target) ?? []), alias]);
}

const root = document.getElementById('editor')!;
root.innerHTML = `
  <style>
    .catalog input { box-sizing: border-box; width: 100%; padding: 10px 14px; border: 1px solid #d0d7de; border-radius: 8px; font: 15px system-ui, sans-serif; }
    .catalog .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); grid-auto-rows: 76px; gap: 8px; max-height: 328px; margin-top: 12px; overflow: auto; }
    .catalog .cell { display: grid; justify-items: center; align-content: center; gap: 8px; min-width: 0; border-radius: 8px; color: #24292f; background: #f6f8fa; font: 11px ui-monospace, monospace; }
    .catalog .cell span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  </style>
  <div class="catalog">
    <input type="search" placeholder="Search ${names.length} icons" aria-label="Search icons" />
    <div class="grid"></div>
  </div>`;

const grid = root.querySelector('.grid')!;
const show = (query: string) => {
  const q = query.trim().toLowerCase();
  const matches = names.filter((name) => !q || name.includes(q) || (aliasesOf.get(name) ?? []).some((alias) => alias.includes(q)));
  grid.innerHTML = matches
    .map((name) => `<div class="cell" title="${name}">${renderIconSvg(name, { size: 28 })}<span>${name}</span></div>`)
    .join('');
};

root.querySelector('input')!.addEventListener('input', (event) => show((event.target as HTMLInputElement).value));
show('');

Style an icon

import { renderIconSvg, type IconRenderOptions } from '@editora/icons';

const samples: Array<[label: string, name: string, options: IconRenderOptions]> = [
  ['default', 'bell', {}],
  ['size: 40', 'bell', { size: 40 }],
  ['color', 'bell', { size: 40, color: '#2563eb' }],
  ['iconWeight: thin', 'bell', { size: 40, iconWeight: 'thin' }],
  ['iconWeight: bold', 'bell', { size: 40, iconWeight: 'bold' }],
  ['strokeWidth: 2.5', 'bell', { size: 40, strokeWidth: 2.5 }],
  ['variant: solid', 'bell', { size: 40, variant: 'solid', color: '#2563eb' }],
  ['variant: duotone', 'shield', { size: 40, variant: 'duotone', color: '#2563eb', secondaryColor: '#93c5fd' }],
  ['arrow-right', 'arrow-right', { size: 40 }],
  ['rotate: 90', 'arrow-right', { size: 40, rotate: 90 }],
  ['flip: horizontal', 'arrow-right', { size: 40, flip: 'horizontal' }],
  ['rtl: true', 'arrow-right', { size: 40, rtl: true }],
];

document.getElementById('editor')!.innerHTML = `
  <style>
    .samples { display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px; }
    .samples figure { display: grid; grid-template-rows: 40px auto; justify-items: center; align-items: center; gap: 12px; margin: 0; padding: 16px 8px 12px; border: 1px solid #d0d7de; border-radius: 10px; color: #24292f; }
    .samples figcaption { font: 12px ui-monospace, monospace; color: #57606a; }
  </style>
  <div class="samples">
    ${samples.map(([label, name, options]) => `<figure>${renderIconSvg(name, options)}<figcaption>${label}</figcaption></figure>`).join('')}
  </div>`;

Every option of renderIconSvg(name, options) and iconToDataUri(name, options):

| Option | Type | Default | Notes | | --- | --- | --- | --- | | variant | 'outline' \| 'solid' \| 'duotone' | 'outline' | Falls back to outline when the icon has no such variant. | | size | number \| string | 15 | Width and height: a number of pixels, or a CSS length such as '1.5rem'. Anything else is ignored. | | color | string | 'currentColor' | The icon's colour. | | secondaryColor | string | color | The second colour of a duotone icon. | | iconWeight | 'thin' \| 'regular' \| 'bold' | 'regular' | Stroke width 1.25, 1.5 or 1.75. | | strokeWidth | number | 1.5 | Stroke width in pixels. Wins over iconWeight; a negative or non-numeric value is ignored. | | strokeLinecap | 'butt' \| 'round' \| 'square' | 'round' | The ends of lines. | | strokeLinejoin | 'miter' \| 'round' \| 'bevel' | 'round' | The corners of lines. | | title | string | none | Adds a <title> and makes the icon an image with that name. | | ariaLabel | string | none | Makes the icon an image with that name, without a <title>. | | decorative | boolean | true unless there is a title or ariaLabel | true hides the icon from screen readers. | | rotate | number | 0 | Degrees, clockwise. | | flip | 'horizontal' \| 'vertical' \| 'both' | none | Mirrors the icon. | | rtl | boolean | false | Mirrors the icons that point a direction (12 of them: arrows, chevrons, carets, reply), and no others. | | className, style | string | none | Set on the <svg>. | | attrs | Record<string, string \| number \| boolean \| undefined> | none | More attributes for the <svg>, such as data-*. A name that could break out of the markup, or that starts with on, is dropped; values are escaped. | | absoluteStrokeWidth | boolean | none | Does nothing: strokes are already the same width on screen at every size. |

Variants

Every icon has an outline. Solid and duotone are drawn for few of them: 21 have a solid variant (check, x, bell, heart, star, home, lock, trash, folder... ) and 2 have a duotone one (chart-pie and shield). Asking for a variant an icon does not have draws its outline, and resolveIcon(name, variant) tells you which one you got.

Accessibility

An icon without a title or a label is decorative: aria-hidden="true" and role="presentation". That is right for an icon next to text, or inside a button that has its own label, as in the toolbar above. An icon that is the only thing saying what a control does needs a name:

renderIconSvg('bell', { title: 'Notifications' });      // role="img" aria-label="Notifications", plus a <title>
renderIconSvg('bell', { ariaLabel: 'Notifications' });  // the same, without the <title> (no tooltip)
renderIconSvg('bell', { decorative: false });           // role="img" aria-label="bell": the icon's own name
renderIconSvg('bell', { title: 'Notifications', decorative: true }); // hidden, whatever the title

Your own icons

An icon is a name, an optional list of aliases and a list of shapes. Shapes without a stroke or a fill are drawn as an outline in the icon's colour, so a few paths are enough. Once registered, an icon is found by renderIconSvg, hasIcon and listIcons like the built-in ones. Registering a name that exists replaces that icon.

import { hasIcon, iconToDataUri, registerIcon, renderIconSvg, type IconDefinition } from '@editora/icons';

// Nodes without a stroke or a fill are drawn as an outline in the icon's colour, so a few paths are enough.
const invoice: IconDefinition = {
  name: 'invoice',
  aliases: ['bill'],
  viewBox: '0 0 24 24',
  variants: {
    outline: {
      nodes: [
        { tag: 'path', attrs: { d: 'M6 3h12v18l-3-2-3 2-3-2-3 2z' } },
        { tag: 'line', attrs: { x1: 9, y1: 8, x2: 15, y2: 8 } },
        { tag: 'line', attrs: { x1: 9, y1: 12, x2: 15, y2: 12 } },
      ],
    },
  },
};

registerIcon(invoice);
console.log(hasIcon('invoice'), hasIcon('bill')); // true true

// As markup, by its name or by an alias...
const markup = renderIconSvg('bill', { size: 32, color: '#2563eb' });

// ...or as a data URI, for an <img> or a CSS background.
const uri = iconToDataUri('invoice', { size: 32, color: '#16a34a' });

document.getElementById('editor')!.innerHTML = `
  <div style="display: flex; align-items: center; gap: 24px">
    <span id="markup">${markup}</span>
    <img id="image" src="${uri}" width="32" height="32" alt="" />
    <span id="background" style="width: 32px; height: 32px; background: url('${uri}') center / contain no-repeat"></span>
  </div>`;

A definition has name, aliases, viewBox ('0 0 24 24' unless you say otherwise), rtlMirror (let rtl mirror it), tags, categories and variants, which must have an outline and may have a solid and a duotone. Each variant is { nodes }, and a node is { tag, attrs?, children? } where tag is path, circle, ellipse, line, polyline, polygon, rect or g. Attribute names can be camelCase (strokeWidth). A node with tone: 'secondary' and a fill or stroke of 'currentColor' is drawn in secondaryColor, which is how a duotone variant has two colours. registerIcons(definitions) registers a list.

In the Editora UI components

@editora/ui-core draws these icons in <ui-icon>, and @editora/ui-react and @editora/react-icons in React. They know every icon already, with no setup, and accept most of these options as attributes or props.

<script type="importmap">
  {
    "imports": {
      "@editora/ui-core": "https://cdn.jsdelivr.net/npm/@editora/ui-core/dist/index.esm.js",
      "@editora/icons": "https://cdn.jsdelivr.net/npm/@editora/icons/dist/index.esm.js"
    }
  }
</script>
<script type="module">
  import '@editora/ui-core';
</script>

<!-- Every icon of @editora/icons is already known to <ui-icon>: there is nothing to register. -->
<div style="display: flex; align-items: center; gap: 20px">
  <ui-icon name="bell" size="28"></ui-icon>
  <ui-icon name="bell" size="28" icon-variant="solid" color="#2563eb"></ui-icon>
  <ui-icon name="chart-pie" size="28" icon-variant="duotone" color="#2563eb" secondary-color="#93c5fd"></ui-icon>
  <ui-icon name="bell" size="28" icon-weight="bold"></ui-icon>
  <ui-icon name="arrow-right" size="28" rotate="90"></ui-icon>
  <ui-icon name="bell" size="28" label="Notifications"></ui-icon>
</div>

An icon you register with this package is found by those components too. Do not register the whole catalog with ui-core yourself (registerWithEditoraUI(registerIcon)): its registry wins over the built-in icons, and every <ui-icon> would then draw the plain outline, whatever its icon-variant or icon-weight.

API

| Export | What it does | | --- | --- | | renderIconSvg(name, options?) | The icon as an <svg> string, or '' if there is no such icon. | | iconToDataUri(name, options?) | data:image/svg+xml;utf8,… for an <img> or url(), or ''. | | listIcons() | Every icon name, sorted, without aliases. | | listIconAliases() | { alias: name } for every alias. | | hasIcon(name) | Whether the name, or an alias, is an icon. | | getIcon(name) | The icon's definition, or undefined. | | resolveIcon(name, variant?) | { definition, viewBox, variant, glyph }: the variant that will be drawn, which is outline if the one asked for is missing. | | registerIcon(definition), registerIcons(definitions) | Add icons, or replace them. | | iconDefinitions, iconNameList | The built-in catalog as data, and its names. | | normalizeIconSize(size, fallback?) | The size renderIconSvg would draw: a positive number or CSS length, else the fallback. | | registerWithEditoraUI(register) | Calls register(name, svg) once for every icon, with its outline as markup: for an icon store of your own. |

The types are IconDefinition, IconGlyph, IconNode, IconTag, IconAttrValue, IconRenderOptions, IconVariant, IconWeight and ResolvedIcon.

Size and where it runs

The catalog is one module: importing anything from the package, even hasIcon, brings all 504 icons. renderIconSvg alone is about 138 KB minified and 48 KB gzipped (there is no per-icon import to tree-shake). If you need a handful of icons on a size-sensitive page, render them once and keep the strings.

Nothing in the package touches the DOM, so it also runs in Node and on the server, and the <svg> strings are the same everywhere. require('@editora/icons') works from Node 14. A bare import in a Node ES module needs Node 22.12 or later, because older versions cannot load the package's ES module build: use require there, or a bundler, which is how a Next.js or Vite server build imports it.

License

MIT © Ajay Kumar