Skip to content

Documentation

Install EVicons and use the icons in React, as plain SVG and on maps.

Install

EVicons ships as two packages with the same icons and one shared version. Use the React components in React apps and the SVG package everywhere else.

Package Contents
@evicons/react React components, tree-shakable ESM with TypeScript types
@evicons/svg Raw SVG files, an SVG sprite and icons.json (names, meanings, tags, aliases)
Shell
npm i @evicons/react
Shell
npm i @evicons/svg

EVicons covers only e-mobility and EV charging. Keep a general icon set such as Lucide for arrows, menus and other UI icons; the two sit side by side.

Compatibility

Supported
React 18 and 19 (peer dependency ^18.0.0 || ^19.0.0)
Server rendering Node.js 20 or later (tested on 20 and 24); works in React Server Components, no 'use client' needed
Module format ESM only, no CommonJS build; require() works on Node.js versions with require(esm) (20.19+, 22.12+)
TypeScript Types included

React

Import an icon by its component name (PascalCase of the file name: plug-type-2 → PlugType2) and put it next to the text it belongs to.

TSX
import { PlugType2 } from '@evicons/react'

export function Connector() {
  return (
    <span>
      <PlugType2 size={20} title="Type 2" /> Type 2 · 22 kW
    </span>
  )
}

Every icon is its own module and the package has sideEffects: false, so bundlers only include the icons you import. A single icon can also be imported directly, as a named or default export:

TSX
import PlugType2 from '@evicons/react/icons/plug-type-2'

<PlugType2 size={20} />

Props

Prop Type Default Description
size number | string 24 Width and height of the <svg>. Numbers are pixels; strings are passed as is (for example "1em").
strokeWidth number | string 1.5 Stroke width in icon units (the icon is drawn on a 24 × 24 grid).
absoluteStrokeWidth boolean false Keep the stroke at strokeWidth screen pixels at every size (see below).
className string Added after the built-in classes evicon and evicon-<name> (for example evicon-plug-type-2).
title string Accessible name, rendered as <title> (see Accessibility).
ref Forwarded to the <svg> element.
…all other SVG props Passed through to the <svg> element (style, onClick, data-*, aria-*, role, …). Children are rendered after the icon’s shapes.

The prop type is exported as EvIconProps, the component type as EvIconComponent.

Without title, aria-label or aria-labelledby an icon is decorative and gets aria-hidden="true". With a title it gets role="img" and is named by a <title> element. The full rules are under Accessibility.

absoluteStrokeWidth

By default the stroke scales with the icon: at size={12} a stroke of 1.5 is drawn 0.75 px wide. With absoluteStrokeWidth, every shape gets vector-effect="non-scaling-stroke", so the stroke is drawn strokeWidth screen pixels wide whatever the rendered size.

TSX
import { CurrentDc } from '@evicons/react'

<CurrentDc size={16} />                                     // stroke 1 px (1.5 × 16 / 24)
<CurrentDc size={16} absoluteStrokeWidth />                 // stroke 1.5 px
<CurrentDc size={48} strokeWidth={1} absoluteStrokeWidth /> // stroke 1 px

Because this is done by the browser and not computed from size, it also works when the size comes from CSS (size="1em", className="size-4", non-square sizes). Shapes you pass as children are not changed.

Server rendering

The components render on the server without 'use client': in Next.js Server Components, in static site generators and with renderToString. The id that links a <title> to its <svg> comes from React’s useId, so it is unique and stable between server and client. Server rendering needs Node.js 20 or later.

TSX
// app/stations/page.tsx: a Server Component
import { PlugCcs2 } from '@evicons/react'

export default function Page() {
  return (
    <p>
      <PlugCcs2 size={20} /> CCS · 150 kW
    </p>
  )
}

SVG & sprite

@evicons/svg has the same icons as plain files, for any framework or none.

Text
@evicons/svg/
├── svg/<name>.svg   one file per icon, exported as @evicons/svg/<name>.svg
├── sprite.svg       all icons as <symbol id="evicon-<name>">
└── icons.json       metadata for every icon

Inline SVG

The files contain no colours; every stroke is currentColor. Inline the markup to colour and size it with CSS. With Vite, for example:

TypeScript
import plugType2 from '@evicons/svg/plug-type-2.svg?raw'

document.querySelector('#connector')!.innerHTML = plugType2

As an <img src> the icon draws in black: currentColor does not reach into an image, so use <img> only where black is what you want.

Sprite

The sprite holds every icon as a <symbol> with the id evicon-<name>. Copy it into your app’s public folder, so that it is served from your own origin (browsers do not load <use> references from other origins):

Shell
cp node_modules/@evicons/svg/sprite.svg public/sprite.svg
HTML
<svg width="24" height="24" aria-hidden="true">
  <use href="/sprite.svg#evicon-plug-type-2" />
</svg>

The icon takes the CSS color of the <svg> or a parent. Page CSS cannot reach the shapes inside a <use>, so filled map markers need inline SVG or the React components instead.

icons.json

Metadata for all icons, for pickers, search and legends:

TypeScript
import icons from '@evicons/svg/icons.json' with { type: 'json' }

// All connectors with their React component name and English meaning.
const connectors = Object.entries(icons)
  .filter(([, icon]) => icon.category === 'connector')
  .map(([name, icon]) => ({ name, component: icon.pascal, label: icon.meaning.en }))

The file is one object keyed by icon name:

JSON
{
  "plug-type-2": {
    "category": "connector",
    "meaning": { "en": "Type 2 connector (IEC 62196-2, \"Mennekes\"), shown as the vehicle inlet face", "de": "…" },
    "tags": { "en": ["type 2", "mennekes", "iec 62196-2", "…"], "de": ["…"], "fr": ["…"] },
    "since": "0.1.0",
    "aliases": [],
    "standard": { "norm": "IEC 62196-2 (Type 2)", "source": "…" },
    "exceptions": ["dense-connector"],
    "provenance": { "createdBy": "…", "approvedBy": "…", "approvedOn": "…" },
    "pascal": "PlugType2"
  }
}
Field Type Description
category string One of connector, station, power, vehicle, energy, status, payment, marker.
meaning { en, de } The precise meaning in English and German.
tags { en, de, fr } Search terms in English, German and French.
since string The version that first shipped the icon.
aliases string[] Former names. @evicons/react also exports the component under these names.
deprecated { since, replacement? } Only on deprecated icons; replacement names the icon to use instead.
standard { norm, source } Connectors only: the standard and the drawing the contact layout was checked against.
exceptions string[] Optional. Deliberate style deviations from the design guide.
provenance { createdBy, approvedBy, approvedOn } How the icon was created, who approved it and when.
pascal string The React component name.

Colouring

Icons draw with currentColor and contain no colours. Set the CSS color of the icon or of a parent; the icon follows the text colour, hover states and dark mode of its surroundings.

TSX
import { ChargingStation } from '@evicons/react'

<p style={{ color: '#2563eb' }}>
  <ChargingStation size={20} /> 12 charge points nearby
</p>

Tailwind CSS

Text colour utilities colour the icon; size utilities override the size attribute.

TSX
import { StatusAvailable } from '@evicons/react'

<StatusAvailable className="text-green-600" title="Available" />
<StatusAvailable className="size-4 text-green-600 dark:text-green-400" title="Available" />

Status colours as CSS variables

Keep the status colours in one place and refer to them from icons, text and map layers. These are the colours used on this site:

CSS
:root {
  --status-available: #16a34a;
  --status-occupied: #d97706;
  --status-faulted: #dc2626;
  --status-out-of-service: #71717a;
  --status-unknown: #0891b2;
  --status-reserved: #7c3aed;
  --status-queue: #c2410c;
}
TSX
import { StatusOccupied } from '@evicons/react'

<StatusOccupied style={{ color: 'var(--status-occupied)' }} title="Occupied" />
<StatusOccupied className="text-(--status-occupied)" title="Occupied" />

With Tailwind CSS v4, declare them as theme colours (@theme { --color-status-occupied: #d97706; }) to get utilities such as text-status-occupied.

Contrast

An icon that carries meaning needs a contrast of at least 3:1 against its background (WCAG 1.4.11). On light backgrounds use #d97706 for occupied (3.19:1 on white): next to text, for fills and for filled map markers. This site uses it everywhere. The lighter amber #f59e0b reaches only 2.15:1 on white, and so does a white sign on a marker in that colour; use it only on dark backgrounds (9.26:1 on #09090b). The table under Status icons lists the contrast of every suggested colour on white.

Status icons

The seven status icons share one round frame; the sign inside carries the meaning. The shape carries the meaning, the colour is yours: the icons read correctly when your app colours them all the same, or not at all.

StatusIconSuggested colourOn whiteMeaning
Availablestatus-available#16a34a3.30:1Available: the charge point is free and ready to charge
Occupiedstatus-occupied#d97706recommended#f59e0bdark backgrounds3.19:12.15:1 (#f59e0b)Below 3:1: dark backgrounds onlyOccupied: the charge point is in use by another vehicle
Faultedstatus-faulted#dc26264.83:1Faulted: the station reports an error and cannot charge
Out of servicestatus-out-of-service#71717a4.83:1Out of service: taken out of service by the operator (for example maintenance or closure), not an error report
Reservedstatus-reserved#7c3aed5.70:1Reserved: the charge point is reserved for a booking
Unknownstatus-unknown#0891b23.68:1Unknown: no current status is available for the charge point
Queuestatus-queue#c2410c5.18:1Queue: cars are queuing at this station; you would have to wait

“Faulted” (the station reports an error) and “out of service” (taken out of service by the operator) are different states; use the one your data actually reports. The colours are suggestions; use your design system’s tokens.

TSX
import {
  StatusAvailable,
  StatusFaulted,
  StatusOccupied,
  StatusOutOfService,
  StatusQueue,
  StatusReserved,
  StatusUnknown,
  type EvIconComponent,
} from '@evicons/react'

type Status = 'available' | 'occupied' | 'faulted' | 'out-of-service' | 'reserved' | 'unknown' | 'queue'

const status: Record<Status, { Icon: EvIconComponent; label: string }> = {
  available: { Icon: StatusAvailable, label: 'Available' },
  occupied: { Icon: StatusOccupied, label: 'Occupied' },
  faulted: { Icon: StatusFaulted, label: 'Faulted' },
  'out-of-service': { Icon: StatusOutOfService, label: 'Out of service' },
  reserved: { Icon: StatusReserved, label: 'Reserved' },
  unknown: { Icon: StatusUnknown, label: 'Unknown' },
  queue: { Icon: StatusQueue, label: 'Queue' },
}

export function StatusLabel({ value }: { value: Status }) {
  const { Icon, label } = status[value]
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
      <Icon size={16} style={{ color: `var(--status-${value})` }} />
      {label}
    </span>
  )
}

The icon is decorative here because the label is visible text.

Status on another icon

Status icons are standalone symbols. To combine a status with another icon (charging station, map pin), place a small status icon absolutely in the bottom-right corner:

TSX
import { ChargingStation, StatusAvailable } from '@evicons/react'

export function StationWithStatus() {
  return (
    <span style={{ position: 'relative', display: 'inline-block', lineHeight: 0 }}>
      <ChargingStation size={24} />
      <StatusAvailable
        size={12}
        absoluteStrokeWidth
        strokeWidth={1.5}
        title="Available"
        style={{ position: 'absolute', right: -4, bottom: -4, color: 'green' }}
      />
    </span>
  )
}

Use a badge size of half the base icon and give it a fixed screen stroke (absoluteStrokeWidth with strokeWidth={1.5}, or strokeWidth={2} without it), otherwise the scaled-down stroke looks faint. The badge covers part of the base icon; on a busy background, put it on a circle in your page’s background colour. EVicons does not reserve a badge corner in the icons yet.

Map markers

The marker-* icons are map pins with a sign in the head: marker-charging is the plain charge point, the others add AC/DC or a status. Use them at 24, 32 or 40 px; they are not drawn for 16 px. Like all icons they are outlines in currentColor, and they can also be drawn filled: the pin in your colour, the sign in white. The SVG files never contain a colour; filling is done by your CSS or your code.

Outline

24 px32 px40 px

Filled

24 px32 px40 px
The same markers as outline and filled with the CSS rule below, at the three supported sizes.

Element order is a contract: in every marker the first <path> is the pin, a single closed shape that encloses the sign. Everything after it is the sign. This order never changes in a released marker.

The charging marker is named marker-charging (MarkerCharging), not marker, so that it does not clash with the Marker component of map libraries (MapLibre, Mapbox, Leaflet, Google Maps).

Filled with CSS

For inline SVG and the React components:

CSS
.marker { color: #1f9d55; }
.marker svg > path:first-of-type { fill: currentColor; }
.marker svg > :not(path:first-of-type) { stroke: #fff; }
TSX
import { MarkerAvailable } from '@evicons/react'

export function AvailablePin() {
  return (
    <span className="marker">
      <MarkerAvailable size={32} title="Available" />
    </span>
  )
}

Select the pin with path:first-of-type, not :first-child: with title, the React component renders a <title> as the first child of the <svg>. The rule works for inline SVG and the React components. It does not work with the sprite: the shapes of a <symbol> shown with <use> are not children of your <svg>, so page CSS cannot select them. The pin keeps its stroke, so a filled pin is 0.75 units larger than its outline; that is intended.

MapLibre GL

Map libraries draw markers as images, where page CSS does not apply. Fill the SVG string before you turn it into an image: set the root stroke="currentColor" to white (the sign inherits it) and give the first <path>, the pin, the fill and stroke colour. Then load it from a data URL and add it with pixelRatio: 2, so it stays sharp on high-density screens:

TypeScript
import type { Map } from 'maplibre-gl'

// `svg` is the text of a file from @evicons/svg, for example `@evicons/svg/marker-charging.svg`
// (with Vite: `import svg from '@evicons/svg/marker-charging.svg?raw'`). `size` is the size on the map in px.
export async function addMarkerImage(map: Map, name: string, svg: string, colour: string, size = 32) {
  const filled = svg
    .replace('width="24" height="24"', `width="${size * 2}" height="${size * 2}"`)
    .replace('stroke="currentColor"', 'stroke="#fff"')
    .replace('<path ', `<path fill="${colour}" stroke="${colour}" `)
  const img = new Image()
  img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(filled)}`
  await img.decode()
  map.addImage(name, img, { pixelRatio: 2 })
}
TypeScript
await addMarkerImage(map, 'ev-marker-charging', svg, '#2563eb')

map.addLayer({
  id: 'charge-points',
  type: 'symbol',
  source: 'charge-points',
  layout: { 'icon-image': 'ev-marker-charging', 'icon-anchor': 'bottom', 'icon-offset': [0, 32 / 12] },
})

The pin’s tip is at y 22 of 24, so with 'icon-anchor': 'bottom' an offset of size / 12 px moves the tip onto the point. Use one image per marker and colour. For an outline marker, only set the size and replace the root stroke="currentColor" with the colour.

Leaflet

Leaflet’s L.divIcon places HTML on the map, so the inline SVG and the CSS rule above work as they are. Size the SVG to the icon box and anchor the tip, which sits at 22/24 of the height:

TypeScript
import L from 'leaflet'
import svg from '@evicons/svg/marker-available.svg?raw'

const size = 32
const icon = L.divIcon({
  html: svg,
  className: 'marker', // replaces Leaflet's default white box; the CSS rule above fills the pin
  iconSize: [size, size],
  iconAnchor: [size / 2, (size * 22) / 24],
})

L.marker([47.856, 12.128], { icon, title: 'Available' }).addTo(map)
CSS
.marker svg { display: block; width: 100%; height: 100%; }

See the map demo for markers on a real map.

Accessibility

Decorative or meaningful

Icons are decorative by default: the <svg> gets aria-hidden="true" and no role. Icons complement visible text; when an icon stands alone and carries meaning, give it an accessible name. As soon as it has one, aria-hidden is dropped and role="img" is set.

If several are given, this order applies:

  1. aria-labelledby (yours): kept as is.
  2. aria-label: used as the name. A title is still rendered as <title> but not referenced.
  3. title: rendered as the first child <title> with an id from React’s useId (unique and stable in server rendering) and referenced with aria-labelledby.
  4. None of these: decorative (aria-hidden="true").

An explicit role or aria-hidden that you pass always wins over the defaults.

TSX
import { StatusFaulted } from '@evicons/react'

<StatusFaulted title="Faulted" />                    // role="img", named "Faulted"
<StatusFaulted aria-label="Charge point faulted" />  // role="img", named by aria-label
<StatusFaulted />                                    // aria-hidden="true"

For inline SVG and the sprite, add the attributes yourself: aria-hidden="true" on decorative icons, role="img" with aria-label on meaningful ones.

Icons complement text

Do not use an icon as the only carrier of information. At 16 px the contacts of a connector face render as similar dots, so faces that share a part are hard to tell apart: plug-type-2 and plug-ccs2 differ only in the DC part below the Type 2 contacts. Likewise plug-chademo and plug-schuko look alike at 16 px (a round face with two lateral contacts). Always show the connector name next to the icon, for example “Type 2 · 22 kW”. The same applies to status icons: show the status as text or at least as an accessible name.

Focus and controls

Icons are not focusable and have no keyboard behaviour of their own. For an icon-only control, use a real <button> or <a>, name the control and leave the icon decorative:

TSX
import { MarkerCharging } from '@evicons/react'

<button type="button" aria-label="Show charge points on the map">
  <MarkerCharging size={24} />
</button>

Keep the focus outline visible and give the control a target of at least 24 × 24 px.

Not by colour alone

Status icons differ in shape, so “available” and “faulted” stay distinguishable without colour, in greyscale and for colour-blind users. Keep it that way in your app: show the status as text next to the icon, and do not tell states apart only by colouring one icon differently. Coloured icons need a contrast of at least 3:1 against their background; see Contrast.

Design principles

Every icon follows the design guide; the linter enforces its technical rules. In short:

  • Grid and stroke: 24 × 24 with at least 1 unit to the edge (live area 1–23), stroke width 1.5, round caps and joins.
  • Outline only: no fills, no colours, no opacity; colour comes from CSS through currentColor.
  • Soft shapes, more detail: outer corners round (radius 1.25 or more) where the subject allows; more inner detail than Lucide, but readable at 16 px and matching Lucide’s optical size at 20 px.
  • Spacing: at least 2.5 units between the centre lines of parallel strokes; 2.0 at the very least where a subject needs it, such as connector faces.
  • Documented exceptions: deliberate deviations are declared per icon: letters, dense-connector, sharp-corner (lightning bolt tips, the marker tip) and solid-sign (the small bolt inside other icons).
  • Letters: AC, DC, kW, P and R come from one stroke alphabet with cap height 8 (5.25 in marker heads) and a gap of 2 units between letters; a lone letter that is the main subject may use cap height 11.
  • Connectors: the vehicle inlet seen from the front, with the contact layout checked against a public drawing of the standard. Schuko, which has no vehicle inlet, shows the socket face.
  • Status: one round frame (<circle cx="12" cy="12" r="9"/>) for every status; the sign inside identifies it without colour.
  • Markers: one standard pin, always the first <path>; the sign stays inside the pin head.
  • Drawn from scratch: no paths copied, traced or adapted from Lucide or any other icon set.

Versioning & changelog

EVicons follows Semantic Versioning. @evicons/react and @evicons/svg share one version, so the same version number always means the same icons in both packages. Every release is listed in the changelog and as a GitHub release.

  • Names are stable. Once released, a name is never changed or removed outside a major version. The element order of a released map marker never changes either.
  • Renaming: the icon gets its new name and keeps the old one as an alias. @evicons/react exports the component under both names, and icons.json lists the alias.
  • Deprecation: a deprecated icon is marked in icons.json (deprecated: { since, replacement }), and its React component gets a @deprecated JSDoc tag, so editors flag its use.
  • Removal: aliases and deprecated icons stay available at least until the next major version. They are removed only in a major release, and the removal is listed in the changelog.

Contributing

EVicons is a small, focused set: icons for e-mobility and EV charging only, no general UI icons and no brand or manufacturer logos. Read CONTRIBUTING.md and the design guide before you draw. A new icon goes like this:

  1. Open an issue with the name, the meaning and, for connectors, the standard, so that these are agreed before drawing.
  2. Draw icons/<name>.svg on the 24 × 24 grid and describe it in icons/<name>.json (category, meaning, tags).
  3. Run npm run lint:icons and npm run build:icons; commit the generated files.
  4. Check the icon with npm run preview at 16, 20 and 24 px, light and dark, next to Lucide (markers at 24, 32 and 40 px, outline and filled).
  5. Open a pull request with a Conventional Commit message; the icon ships once it is approved after an independent review.

All geometry is drawn from scratch; a test fails if an element is identical to one from Lucide. Changes for users go into the Unreleased section of the changelog.