Phosphor Icons: The Complete React Guide (6 Weights, Duotone & More)

Everything about @phosphor-icons/react — all 6 icon weights, the duotone system, SSR setup, IconContext for global defaults, and when to choose Phosphor over Lucide.

Amit Yadav
Amit Yadav

Phosphor Icons is the only major free icon library where every icon ships in six visual weights: Thin, Light, Regular, Bold, Fill, and Duotone. That single feature makes Phosphor uniquely suited to design systems that need to express hierarchy, state, or brand personality through icon weight — not just size and color.

This guide covers the full Phosphor API, the weight system in depth, the duotone color approach, and how to build a scalable Phosphor icon layer for React.

Why Phosphor over Lucide or Tabler

FeaturePhosphorLucideTabler
Icons1,200+1,500+5,000+
Weights/variants61 (outline)2 (line + filled subset)
Duotone supportNativeNoneNone
SSR-safeYesYesYes
MIT licenseYesYesYes

Lucide wins on icon count. Tabler wins on coverage. Phosphor wins on flexibility per icon — if your design system uses weight to communicate hierarchy or importance, there is no better free option.

Installation

npm install @phosphor-icons/react
# or
pnpm add @phosphor-icons/react

Requires React 16.8+. Version 2.x (current) requires React 18+ for full concurrent mode support.

The six weight system

Every Phosphor icon is available in six weights from the same import path:

import {
  Heart,        // Regular (default)
  HeartThin,    // Thin
  HeartLight,   // Light
  HeartBold,    // Bold
  HeartFill,    // Fill
  HeartDuotone, // Duotone
} from '@phosphor-icons/react';

Or use the weight prop on the base component:

import { Heart } from '@phosphor-icons/react';

// All six weights via prop
<Heart weight="thin"     size={32} />
<Heart weight="light"    size={32} />
<Heart weight="regular"  size={32} />  // default
<Heart weight="bold"     size={32} />
<Heart weight="fill"     size={32} />
<Heart weight="duotone"  size={32} />
weight prop vs named imports

Both approaches tree-shake identically. Use the named import (HeartFill) when the weight is fixed at compile time — it reads more clearly in JSX. Use the weight prop when weight is driven by component state (e.g., active/inactive toggle).

When to use each weight

WeightVisualUse case
ThinHairline strokeEditorial, luxury, whitespace-heavy UIs
LightFine strokeSecondary actions, metadata, timestamps
RegularStandard strokeBody text complement, general UI
BoldHeavy strokePrimary actions, empty states, illustrations
FillSolid shapeActive states, selected items, CTAs
DuotoneTwo-colorFeatured items, illustrations, data viz

The most powerful pattern: Regular for inactive, Fill for active. This is the most semantically legible active-state pattern available in any free icon library.

function TabItem({ icon, label, active }) {
  const Icon = active ? `${icon}Fill` : icon; // simplified
  // Better approach:
  return (
    <a className="flex flex-col items-center gap-1">
      <PhosphorIcon
        icon={icon}
        weight={active ? 'fill' : 'regular'}
        size={24}
        className={active ? 'text-blue-600' : 'text-gray-500'}
      />
      <span className="text-xs">{label}</span>
    </a>
  );
}

Duotone icons

Duotone weight renders two-layered icons — a primary element at full opacity and a secondary background element at 20% opacity by default.

Duotone Color Playground

Adjust two-tone colors and opacity before applying them to your icon set.

The secondary layer color is inherited from the icon’s color prop and rendered at opacity: 0.2. To use a different secondary color, wrap the icon and override the inner path directly:

// Default duotone — secondary is 20% of the primary color
<Heart weight="duotone" size={32} color="#2563eb" />

// Custom secondary via CSS
<span style={{ '--ph-duotone': '#93c5fd' } as React.CSSProperties}>
  <Heart weight="duotone" size={32} color="#2563eb" />
</span>
Duotone works automatically in dark mode

Because duotone secondary opacity is relative to color (not a hardcoded value), switching color to a lighter shade in dark mode keeps the duotone relationship correct. Use CSS variables mapped to dark-mode values for consistent results.

Duotone for featured/highlighted UI

Duotone icons communicate “special” or “highlighted” states — useful for:

  • Empty state illustrations (hero-style centered icons)
  • Featured items in a grid
  • Active/selected cards in a list
  • Category badges with icon + label
<div className="flex flex-col items-center gap-4 p-8 text-center">
  <FolderOpen weight="duotone" size={64} color="#2563eb" />
  <h3 className="text-lg font-semibold">No files yet</h3>
  <p className="text-sm text-muted-foreground">Upload your first file to get started.</p>
</div>

IconContext — global defaults

Like Tabler’s IconContext, Phosphor provides a context provider for global defaults:

import { IconContext } from '@phosphor-icons/react';

// Set global size, weight, color, and mirroring
export function App() {
  return (
    <IconContext.Provider value={{
      weight: 'light',
      size: 20,
      color: 'currentColor',
      mirroring: false,
    }}>
      <YourApp />
    </IconContext.Provider>
  );
}

Individual icon props override the context:

// Context: weight="light", size=20
<Gear />                         // weight="light", size=20
<Gear weight="bold" />           // weight="bold", size=20
<Gear size={32} />               // weight="light", size=32

SSR and Server Components

Phosphor icons are SSR-safe — they render to static SVG elements with no client-side state.

// Next.js App Router — works in Server Components
// app/dashboard/page.tsx (no 'use client' needed)
import { ChartBar } from '@phosphor-icons/react';

export default function DashboardPage() {
  return (
    <h1 className="flex items-center gap-2">
      <ChartBar weight="duotone" size={28} />
      Analytics
    </h1>
  );
}

Building an icon system with Phosphor

For a design system, define your icon weight semantics once and reference them everywhere:

// src/lib/iconWeights.ts
export const iconWeights = {
  default:   'regular',
  inactive:  'light',
  active:    'fill',
  featured:  'duotone',
  emphasis:  'bold',
} as const;
// src/components/Icon.tsx
import { iconWeights } from '@/lib/iconWeights';
import type { Icon as PhosphorIcon } from '@phosphor-icons/react';

interface IconProps {
  icon: typeof PhosphorIcon;
  variant?: keyof typeof iconWeights;
  size?: number;
  className?: string;
}

export function Icon({ icon: PhIcon, variant = 'default', size = 20, className }: IconProps) {
  return (
    <PhIcon
      weight={iconWeights[variant]}
      size={size}
      className={className}
    />
  );
}

Usage:

<Icon icon={Heart} variant="active" size={24} className="text-blue-600" />
<Icon icon={Star} variant="featured" size={32} className="text-amber-500" />

Phosphor vs Lucide: when to choose which

Choose Phosphor when:

  • Your design system needs active/inactive state via fill vs outline
  • You want duotone icons for illustrations or featured states
  • You need visual weight variation (editorial, bold, minimal) across the same icon
  • Your team uses Figma and wants weight mapped to Figma component properties

Choose Lucide when:

  • You need more than 1,200 icons
  • You want createLucideIcon for fully custom icons that blend in
  • You’re already using shadcn/ui (Lucide is the default)
  • You want the largest community and most StackOverflow answers
Note

You don’t have to pick just one. Use Lucide as your base library for coverage, and add Phosphor for icons where duotone or weight variation add semantic value. The visual styles are close enough to coexist in the same UI.

Frequently asked questions

Does Phosphor have an icon search? Yes — phosphoricons.com and AllSVGIcons (filter by ph: prefix). Both let you preview all 6 weights before picking.

How many unique icons are there? About 1,200 base icons × 6 weights = ~7,200 total named exports. The package.json size is larger than Lucide’s but individual imports are comparable in size.

Do all 1,200 icons have all 6 weights? Yes — every icon in the library ships all six weights. This is a design requirement Phosphor enforces for every new icon.

Share this post