A small twist, applied everywhere.
Phosphor Light is the geometric backbone. The Pitvolt twist is two marks: a clay-red corner notch on the top-right, and a yellow voltage spark at the lower-left. Both apply via CSS, so the icon family scales without per-icon redrawing.
Anatomy
What every icon is composed of, top to bottom.
-
GlyphPhosphor Light. Inherits
currentColorfrom text. Stroke 1.25. -
Notch - top right22% triangle cut from the corner, filled with
--accent-secondary. -
Spark - lower left14% dot,
--accent-primarywith a soft glow. The voltage signal.
Sizes
Notch and spark scale up at small sizes so the brand reads at 14px. They scale down at large sizes so the glyph stays the hero.
The library
First pass: 26 icons, grouped by use. Add to PvIcon.jsx's registry to extend.
Variants
Outline is default. Filled for active/selected. Tones for status. plain drops the twist when an icon needs to disappear into UI.
In context
How they actually look inside the page patterns you've already built.
Reserve a stall at one of our partner sites - most go live within 2 weeks of contract signing.
Have a question? Email Joojo
Usage
One component. One prop. Twist is automatic.
// 1. Install npm i react-icons // 2. Import once import './icons.css'; import { PvIcon } from './PvIcon'; // 3. Use <PvIcon name="plug" /> <PvIcon name="lightning" size="lg" /> <PvIcon name="warning" tone="warning" title="Charger offline" /> <PvIcon name="check" filled /> <PvIcon name="external-link" plain /> // no twist
The component
PvIcon.jsx in full. It lives in the repo at brand/components/PvIcon.jsx; copy it alongside icons.css.
Show PvIcon.jsx (138 lines) โ
// =============================================================
// Pitvolt - <PvIcon> React wrapper
//
// Single component. Single prop. Applies the brand "twist" (clay-red
// corner notch + yellow voltage spark) to any react-icons glyph.
//
// Usage:
// import { PvIcon } from './PvIcon';
// <PvIcon name="plug" />
// <PvIcon name="lightning" size="lg" />
// <PvIcon name="warning" tone="warning" />
// <PvIcon name="check" filled />
// <PvIcon name="external-link" plain /> // no twist
//
// Requires: npm i react-icons
// And: import './icons.css' (in your entrypoint)
// =============================================================
import React from 'react';
import {
// Nav / UI
PiList, PiX, PiArrowUpRight, PiCaretDown, PiArrowSquareOut, PiMagnifyingGlass,
// Charging
PiPlug, PiLightning, PiBatteryHigh, PiGauge, PiChargingStation, PiCar,
// Host
PiMapPin, PiBuildings, PiKey, PiCalendarBlank,
// Status
PiCheck, PiWarning, PiInfo, PiXCircle,
// Social
PiInstagramLogo, PiXLogo, PiLinkedinLogo, PiWhatsappLogo, PiEnvelope,
} from 'react-icons/pi';
// Map friendly names โ react-icons component
const REGISTRY = {
// Nav / UI
'menu': PiList,
'close': PiX,
'arrow': PiArrowUpRight,
'chevron-down': PiCaretDown,
'external-link': PiArrowSquareOut,
'search': PiMagnifyingGlass,
// Charging
'plug': PiPlug,
'lightning': PiLightning,
'battery': PiBatteryHigh,
'gauge': PiGauge,
'station': PiChargingStation,
'car': PiCar,
// Host
'map-pin': PiMapPin,
'building': PiBuildings,
'key': PiKey,
'calendar': PiCalendarBlank,
// Status
'check': PiCheck,
'warning': PiWarning,
'info': PiInfo,
'x-circle': PiXCircle,
// Social
'instagram': PiInstagramLogo,
'x': PiXLogo,
'linkedin': PiLinkedinLogo,
'whatsapp': PiWhatsappLogo,
'mail': PiEnvelope,
};
export const ICON_NAMES = Object.keys(REGISTRY);
/**
* @param {Object} props
* @param {string} props.name - one of ICON_NAMES
* @param {'xs'|'sm'|'md'|'lg'|'xl'|number|string} [props.size='md']
* Preset key, or a px/css value. Numbers become px.
* @param {boolean} [props.filled] - solid glyph instead of outline
* @param {boolean} [props.plain] - drop the brand twist (notch + spark)
* @param {'default'|'warning'|'success'|'info'} [props.tone='default']
* @param {string} [props.title] - accessible label; if omitted, icon is aria-hidden
* @param {string} [props.className] - extra classes on wrapper
* @param {Object} [props.style] - extra inline styles on wrapper
*/
export function PvIcon({
name,
size = 'md',
filled = false,
plain = false,
tone = 'default',
title,
className = '',
style,
...rest
}) {
const Glyph = REGISTRY[name];
if (!Glyph) {
if (typeof console !== 'undefined') {
console.warn(`[PvIcon] unknown name "${name}". Known names:`, ICON_NAMES);
}
return null;
}
const sizePresets = ['xs', 'sm', 'md', 'lg', 'xl'];
const sizeClass = sizePresets.includes(size) ? `pv-icon--${size}` : '';
const sizeStyle =
!sizePresets.includes(size)
? { '--pv-icon-size': typeof size === 'number' ? `${size}px` : size }
: null;
const classes = [
'pv-icon',
sizeClass,
filled && 'pv-icon--filled',
plain && 'pv-icon--plain',
tone !== 'default' && `pv-icon--${tone}`,
className,
].filter(Boolean).join(' ');
const a11y = title
? { role: 'img', 'aria-label': title }
: { 'aria-hidden': true };
return (
<span
className={classes}
style={{ ...sizeStyle, ...style }}
{...a11y}
{...rest}
>
<Glyph />
{!plain && tone !== 'warning' && tone !== 'success' && (
<span className="pv-icon__notch" aria-hidden="true" />
)}
{!plain && (
<span className="pv-icon__spark" aria-hidden="true" />
)}
</span>
);
}
export default PvIcon;