@doitrix/icons
| Install | |
|---|---|
npm install @doitrix/icons |
|
| Latest Version: | 1.5.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 8, 2026 |
| Links: | npm |
@doitrix/icons
An icon system for interfaces that move — 24×24 geometry, variants derived from one shape, and state transitions that read as a single object changing rather than two icons swapping.
bun add @doitrix/icons
No runtime dependencies. Vue is an optional peer, needed only for the Vue layer.
What this is for
Lucide, Heroicons and Material Symbols are excellent, and this does not try to out-draw them — 129 icons against Lucide's 2,000 is not a contest. It exists to do the things a general-purpose set is not trying to do:
| One geometry, three variants | Outline, duotone and filled are the same paths at different fill strengths — so a variant change is an animatable value, not a swap |
| Any icon morphs into any other | Not curated pairs — home into settings, search into spark. Arbitrary geometry, no hand-alignment |
| Transitions, not swaps | Menu ⇄ Close, Plus ⇄ Close, and a tick that draws on. Continuity is the point |
| Hit area separated from icon size | An 18px icon still gets a 44px target. The most common icon-button mistake, invisible until someone taps it |
| Journey step states | ○ → ◉ → ✓ as one shape, for onboarding progress |
| Onboarding vocabulary | welcome, discover, click, swipe, drag, scroll, tip, shortcut, newFeature |
| Optical sizing | Stroke lightens at small and large sizes so weight reads constant |
| Accessibility by construction | Decorative by default; a label switches it to role="img" |
| Reduced motion | Every icon reaches its final state; it just gets there instantly |
Works in any framework
The core is framework-free. Vue components are one adapter, not the only door.
Plain JavaScript, React, Svelte, Angular, Astro
import { renderIcon, createIcon, defineIconElement } from '@doitrix/icons'
renderIcon('arrowRight', { size: 20 }) // SVG markup as a string
createIcon('home', { size: 24 }) // a live SVGElement
Or use the custom element, which every framework renders natively:
import { defineIconElement } from '@doitrix/icons'
defineIconElement()
<dx-icon name="arrow-right" size="20"></dx-icon>
<dx-icon name="close" label="Dismiss"></dx-icon>
Names accept kebab-case in markup and camelCase in code.
Vue
<script setup lang="ts">
import { DxIcon, DxIconButton, DxJourneyStep } from '@doitrix/icons/vue'
import '@doitrix/icons/style.css'
</script>
<template>
<DxIcon name="arrowRight" :size="20" />
<DxIconButton icon="close" label="Dismiss" context="toolbar" />
<DxJourneyStep state="current" />
</template>
The stylesheet is only needed for the animated components.
Props
DxIcon
| Prop | Type | Default | |
|---|---|---|---|
name |
IconName |
— | Unknown names render nothing rather than breaking layout |
size |
number | string |
24 |
|
strokeWidth |
number | string |
2 |
|
absoluteStrokeWidth |
boolean |
false |
Keep rendered thickness constant as size changes |
fill |
number | boolean |
0 |
0 outline, 1 solid, between is duotone |
rotate |
number |
0 |
About the canvas centre |
flip |
'horizontal' | 'vertical' | 'both' |
— | |
label |
string |
— | Omit for decorative icons |
DxIconButton
| Prop | Type | Default | |
|---|---|---|---|
icon |
IconName |
— | |
label |
string |
— | Required — an icon-only control has no visible name |
context |
IconContext |
'button' |
toolbar button navigation hero status inline |
hitArea |
number |
from context | Never below 44px |
variant |
IconVariant |
'outline' |
|
optical |
boolean |
true |
Adjust stroke for the rendered size |
Animated
DxMenuToClose and DxPlusToClose take open; DxCheckmark takes checked
and circle; DxArrowMotion takes direction and travel; DxLoading takes
duration. All accept size, strokeWidth and label.
DxArrowMotion responds to the parent's hover and focus, so the whole
control moves as one object:
<button class="dx-hover">
Continue <DxArrowMotion direction="right" />
</button>
Morphing
Libraries that morph icons do it by hand: a menu and a close are drawn as the same three lines so CSS can rotate one into the other. That works for a handful of curated pairs and not at all for the rest.
This morphs arbitrary geometry.
<DxMorphIcon :name="current" :size="28" />
Change name and the icon transforms into the new one — any icon, any other
icon, with no pre-alignment.
import { planMorph, canMorph } from '@doitrix/icons'
const plan = planMorph(paths.home, paths.settings)
plan?.at(0.5) // the geometry halfway between them
How it works. Both icons are sampled into equal point counts using the
browser's own geometry engine (getPointAtLength), then interpolated. Using
the browser rather than a hand-written path parser means arcs, cubics,
quadratics and every shorthand come for free, and the sampling cannot drift
from how the path actually renders.
Sub-paths are padded on both sides, so a five-path icon can morph into a two-path icon: the surplus collapses to a point instead of vanishing. Point rings are rotated to their nearest alignment first, which is what stops the shape visibly unwinding mid-morph.
Where no geometry engine exists — a server, an old browser, reduced motion —
canMorph() reports false and the component swaps instantly. The end state
is identical either way.
Icon set
129 icons across twelve groups — navigation, journey, status, onboarding,
interface, communication, people, places, work, data, money and meeting.
import { iconNames, iconGroups, isIconName } from '@doitrix/icons'
Motion tokens
import { DURATION, EASE, prefersReducedMotion } from '@doitrix/icons'
DURATION // { micro: 0.18, state: 0.28, morph: 0.36 }
Shared with @doitrix/product-journey
so icon motion matches journey motion.
Design rules
24 × 24 canvas
2px centred stroke
round caps and joins
legible at 16px
no gradients, no mixed weights
Size
index.js 1.7 KB gzip framework-free core
vue.js 2.3 KB gzip Vue components
paths.js 2.0 KB gzip geometry only
style.css 2.0 KB gzip animated components only
Prior art and credits
This set stands on conventions established by others, and it is worth naming them.
Lucide (ISC) — the design language here follows
Lucide's published guidelines: a 24×24 canvas, 2px centred stroke, round caps
and joins. The absoluteStrokeWidth prop is Lucide's name for that behaviour
and is kept deliberately, because developers already know it.
Material Symbols (Apache-2.0) — the idea of an icon varying continuously along axes such as fill and weight, rather than shipping a separate file per state, comes from Material's variable font work.
Heroicons (MIT) — for the outline/solid pairing that this generalises into a single fill value.
On geometry overlap
Every path here was authored by hand rather than copied. That said, this set was diffed against all 2,025 Lucide icons, and 21 of its 126 paths are identical — all of them primitive: straight lines, dots, and a chevron.
A horizontal line across a 24-unit grid has one sensible form; so does a plus,
a minus, and M12 8h.01 for a dot. Those matches are convergence, not
authorship. No multi-curve icon matched. The comparison is reproducible if you
want to run it yourself.
Universal shapes — a house for home, a magnifier for search, three bars for a menu — are shared visual vocabulary and belong to no one.
License
MIT
Related Packages
Use Font Awesome icons with popular Nuxt modules Nuxt Icon and NuxtUI
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 1.5.0 | ^3.3.0 | MIT | |
| 1.4.1 | ^3.3.0 | MIT | |
| 1.4.0 | ^3.3.0 | MIT | |
| 1.3.0 | ^3.3.0 | MIT | |
| 1.2.0 | ^3.3.0 | MIT | |
| 1.1.0 | ^3.3.0 | MIT | |
| 1.0.1 | ^3.3.0 | MIT | |
| 1.0.0 | ^3.3.0 | MIT |