npm.io
0.2.0 • Published 3 weeks ago

@purposeinplay/feral-icons

Licence
MIT
Version
0.2.0
Deps
0
Size
1.6 MB
Vulns
0
Weekly
0

@purposeinplay/feral-icons

Custom React icon library with full tree-shaking support and TypeScript autocomplete.

License: MIT Commitizen friendly

Features

  • Name-based API - <FeralIcon name="camera" /> - simple and clean
  • Tree-shakable - Only imports what you use
  • Tiny bundle - ~0.3KB per icon gzipped
  • TypeScript - Full type safety with icon name autocomplete
  • Fast - Optimized SVGs with SVGO
  • Customizable - Control size, color, and all SVG props
  • Easy to extend - Add new icons by dropping SVG files

Installation

npm install @purposeinplay/feral-icons
# or
yarn add @purposeinplay/feral-icons
# or
pnpm add @purposeinplay/feral-icons

Usage

Basic Usage
import { FeralIcon } from "@purposeinplay/feral-icons";

function App() {
  return (
    <>
      <FeralIcon name="arrow-left" size={24} />
      <FeralIcon name="camera" fill="red" />
      <FeralIcon name="user" className="text-blue-500" fill="#3B82F6" />
    </>
  );
}
With Dynamic Names
import { FeralIcon, type IconName } from "@purposeinplay/feral-icons";

function IconDisplay({ iconName }: { iconName: IconName }) {
  return <FeralIcon name={iconName} size={24} />;
}

// Usage
<IconDisplay iconName="camera" />;
With Tailwind CSS

Icons work seamlessly with Tailwind CSS color utilities:

// Using Tailwind text color classes (recommended)
<FeralIcon name="camera" className="text-blue-600" />
<FeralIcon name="user" className="text-red-500 hover:text-red-700" />

// Using Tailwind colors with fill prop
<FeralIcon name="arrow-left" fill="rgb(59 130 246)" />

// Responsive colors
<FeralIcon name="camera" className="text-blue-500 md:text-purple-500" />

Pro tip: Since icons default to fill="currentColor", you can control their color with Tailwind's text-* utility classes!

Sprite mode (fewer DOM nodes) — opt-in

FeralIcon renders each icon as an inline <svg> with its paths. On pages with many icons that inflates the DOM. For a lighter footprint, opt into sprite rendering: each icon becomes a 2-node <svg><use/></svg> referencing a single shared sprite.

  1. Render <FeralSprite /> once near your app root (it injects a hidden <svg> of all icon symbols). It's SSR-safe — no asset URL, no import.meta.url, no hydration mismatch.
  2. Use <SpriteIcon /> anywhere instead of <FeralIcon />.
import { FeralSprite, SpriteIcon } from "@purposeinplay/feral-icons";

// once, at the root (e.g. app/layout.tsx)
export default function RootLayout({ children }) {
  return (
    <body>
      <FeralSprite />
      {children}
    </body>
  );
}

// anywhere
<SpriteIcon name="camera" size={24} />
<SpriteIcon name="user" className="text-blue-500" fill="#3B82F6" />

SpriteIcon takes the same name / size / fill / className props as FeralIcon. Coloring works identically — symbols carry no baked fills, so currentColor and the fill prop flow through <use> to the paths.

FeralIcon is unchanged and remains the default; sprite mode is purely additive.

Hosting the sprite as an external file instead? Pass spriteUrl to SpriteIcon (e.g. spriteUrl="/icons/feral-sprite.svg") and serve the feral-sprite.svg shipped in dist/. The same-document default (via <FeralSprite />) is recommended — it needs no per-app bundler config.

Props

The FeralIcon component accepts these props:

  • name: IconName - Required. The icon name (e.g., "camera", "user", "arrow-left")
  • size?: number | string - Icon size (default: 24)
  • fill?: string - Fill color (default: 'currentColor')
  • className?: string - Additional CSS classes
  • All standard SVG attributes (onClick, onMouseEnter, etc.)

Available Icons

  • arrow-left - Navigation arrow pointing left
  • camera - Camera icon
  • user - User profile icon

All icon names are available as the IconName type for TypeScript autocomplete.

Tree-Shaking

This library is fully tree-shakable. Only imported icons are included in your bundle.

Example bundle impact:

  • Importing all icons: ~2KB gzipped
  • Importing single icon: ~0.3KB gzipped

Local Development & Testing

View Icons in Storybook

Run Storybook to browse and interact with all icons:

npm run storybook

This will open Storybook at http://localhost:6006 where you can:

  • Browse all available icons
  • Test different sizes and colors
  • Try Tailwind CSS integration
  • Copy code examples
  • Test accessibility features
# In the @purposeinplay/feral-icons directory
npm link

# In your app directory
npm link @purposeinplay/feral-icons
Use in Your App
import { FeralIcon } from "@purposeinplay/feral-icons";

function MyComponent() {
  return <FeralIcon name="arrow-left" size={32} fill="#3B82F6" />;
}

Adding New Icons

Quick Start
  1. Add your SVG file to the svgs/ directory

    # Example: Add a new icon called "heart"
    cp ~/Downloads/heart.svg svgs/heart.svg
  2. Regenerate the icon pack so the new icon is registered and available

    npm run generate

    This creates/updates the registry and components. You can now use the icon in code and see it in Storybook.

  3. Build the library (when preparing a release)

    npm run build

    This runs generate plus the full library build.

  4. Use your new icon

    <FeralIcon name="heart" size={24} />

That's it! The build process automatically:

  • Optimizes the SVG
  • Generates a React component (Heart.tsx)
  • Adds it to the icon registry
  • Updates TypeScript types for autocomplete
SVG Requirements

Your SVG files should follow these guidelines:

Required:

  • 19x19 viewBox (e.g., viewBox="0 0 19 19")
  • Path-based design with filled paths (not strokes)
  • Kebab-case naming (e.g., arrow-left.svg, user-circle.svg)

Recommended:

  • Use fill="currentColor" or no fill attribute (will inherit from parent)
  • Remove any stroke attributes (optimizer will strip them anyway)
  • Remove unnecessary attributes like id, class, data-*

Example SVG:

<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 19 19">
  <path d="M9.5 2L2 6v8c0 4.44 3.07 8.59 7.5 9.6 4.43-1.01 7.5-5.16 7.5-9.6V6l-7.5-4z"/>
</svg>
Icon Naming Convention

SVG filenames are converted to icon names:

  • arrow-left.svgname="arrow-left"
  • user-circle.svgname="user-circle"
  • chevron-down.svgname="chevron-down"

Component names are PascalCase internally:

  • arrow-left.svgArrowLeft component
  • user-circle.svgUserCircle component

Development Scripts

Generating Icons
# Generate components from SVGs (without building)
npm run generate

Versioning and Releases

We use standard-version to automate Semantic Versioning and CHANGELOG generation based on Conventional Commits.

Creating a Release

To create a new version:

  1. Commit your changes using Conventional Commit messages (e.g., feat: ..., fix: ...).
  2. Run the release script:
    # Standard release (automatic version bump)
    npm run release
    
    # Or specify the bump type explicitly
    npm run release:patch
    npm run release:minor
    npm run release:major

This command will:

  • Bump the version in package.json and package-lock.json.
  • Generate/update CHANGELOG.md.
  • Create a git commit and tag (e.g., v0.1.5).
  1. Push to origin:
    git push --follow-tags origin main

Development Workflow

Adding/updating icons:

# 1. Add SVG to svgs/ directory
cp new-icon.svg svgs/

# 2. Regenerate the pack (registers the icon; use this to see it in Storybook)
npm run generate

# 3. Full rebuild (when preparing a release)
npm run build

# Icons are now available!

During development:

# Watch mode - rebuilds on changes
npm run dev

# In another terminal, run Storybook
npm run storybook
Testing Changes Locally
# In the icon library directory
npm link

# In your app directory
npm link @purposeinplay/feral-icons

# Make changes, rebuild, and test
npm run build

How It Works

Architecture Overview
User Code                Internal System              SVG Files
─────────                ────────────────             ─────────

<FeralIcon              →  iconRegistry               svgs/
  name="camera"            {                           ├── arrow-left.svg
  size={24}                 'camera': Camera,         ├── camera.svg
/>                          'user': User,             └── user.svg
                            ...                             ↓
                          }                           [Build Process]
                             ↓                              ↓
                         Camera component            Generated Components
                         renders with props          ├── ArrowLeft.tsx
                                                     ├── Camera.tsx
                                                     └── User.tsx
Build Process

When you run npm run build, here's what happens:

  1. Clean - Removes old generated files
  2. Generate - For each SVG in svgs/:
    • Optimizes SVG with SVGO (removes strokes, dimensions, etc.)
    • Extracts SVG path content
    • Generates React component file
    • Adds to icon registry
    • Updates TypeScript types
  3. Compile - TypeScript generates .d.ts files
  4. Bundle - Vite creates ESM output with tree-shaking
File Structure
feral-icon-pack/
├── svgs/                    # Source SVG files (you add here)
│   ├── arrow-left.svg
│   ├── camera.svg
│   └── user.svg
├── src/
│   ├── icons/              # Generated (gitignored)
│   │   ├── ArrowLeft.tsx
│   │   ├── Camera.tsx
│   │   └── User.tsx
│   ├── shared/
│   │   ├── Icon.tsx        # Base SVG wrapper
│   │   └── types.ts        # Internal types
│   ├── iconRegistry.ts     # Generated (gitignored)
│   ├── FeralIcon.tsx       # Public component
│   └── index.ts            # Generated (gitignored)
├── dist/                   # Build output (gitignored)
│   ├── index.js
│   ├── index.d.ts
│   ├── FeralIcon.js
│   └── ...
└── scripts/
    ├── generate.ts         # SVG → React converter
    └── optimize.ts         # SVGO configuration
Package Exports

Only these are exported publicly:

// From @purposeinplay/feral-icons
export { FeralIcon } from "./FeralIcon";
export type { FeralIconProps, IconName } from "./FeralIcon";

Everything else (individual icon components, base Icon, internal types) stays internal for the registry system.

TypeScript Support

This library is written in TypeScript and includes full type definitions.

import { FeralIcon, type FeralIconProps, type IconName } from "@purposeinplay/feral-icons";

// Type for icon props
const CustomIconWrapper: React.FC<FeralIconProps> = (props) => {
  return <FeralIcon {...props} />;
};

// Type for icon names (with autocomplete)
const iconName: IconName = "camera"; // TypeScript will suggest: "arrow-left" | "camera" | "user"

// Dynamic icon selection with type safety
function DynamicIcon({ name }: { name: IconName }) {
  return <FeralIcon name={name} size={24} />;
}

Available Types:

  • IconName - Union type of all valid icon names ("arrow-left" | "camera" | "user")
  • FeralIconProps - Props interface for the FeralIcon component (name, size, fill, className, etc.)

Using with Other Icon Libraries

If you need to use both Feral icons and other icon libraries (like lucide-react) in the same project, see DYNAMIC_ICON.md for a complete implementation guide.

The DYNAMIC_ICON.md file provides:

  • A production-ready DynamicIcon component that supports both Feral and Lucide icons
  • Prefix-based icon selection (lucide:Heart vs feral:check)
  • Auto-detect mode for seamless integration
  • Full TypeScript support
  • Usage examples and migration guides

Note: The DynamicIcon component is not exported by this package. It's a template you can copy into your project for advanced use cases.

Browser Support

  • Modern browsers with ES2020 support
  • React 18.0.0 or higher
  • React 19.x supported

License

MIT

Contributing

Adding Icons
  1. Fork and clone this repository
  2. Add SVG files to svgs/ directory
  3. Follow SVG requirements (24x24, fill-based, kebab-case)
  4. Regenerate the pack and test
    npm run generate   # Registers the new icon(s)
    npm run storybook # Verify in Storybook
    npm run build     # Full build when ready
  5. Commit and PR
    git add svgs/your-icon.svg
    npm run commit  # Use Commitizen for standardized commits
Commit Messages

This project uses Commitizen for standardized commit messages following the Conventional Commits specification.

Using Commitizen (recommended):

# Stage your changes
git add .

# Use commitizen to create the commit
npm run commit

This will prompt you with:

  • Type: feat, fix, docs, style, refactor, perf, test, build, ci, chore
  • Scope: Optional (e.g., storybook, icons, docs)
  • Description: Short summary of changes
  • Body: Optional detailed description
  • Breaking changes: Optional breaking change notes
  • Issues: Optional issue references

Manual commits are also allowed but should follow this format:

git commit -m "type(scope): description"

Examples:

feat(icons): add heart icon
fix(storybook): correct copy overlay behavior
docs(readme): update installation instructions
refactor(generate): simplify SVG optimization
Guidelines
  • One icon per file
  • Use semantic names (chevron-down, not icon-1)
  • Test in Storybook before submitting
  • Include icon source/license if applicable
  • Use npm run commit for consistent commit messages

Troubleshooting

Icon not showing up?
  1. Check the SVG file name

    ls svgs/
    # Should see your-icon.svg
  2. Rebuild the library

    npm run build
  3. Check TypeScript autocomplete

    <FeralIcon name="your-icon" />
    //              ^ Should autocomplete
TypeScript errors?

If you see Type '"my-icon"' is not assignable to type 'IconName':

  1. Make sure you rebuilt: npm run build
  2. Restart your TypeScript server (VS Code: Cmd+Shift+P → "Restart TS Server")
  3. Check that src/iconRegistry.ts includes your icon
Icons look wrong?
  • Check SVG uses fill not stroke
  • Verify 19x19 viewBox
  • Remove hardcoded colors (SVGO will strip fill attributes automatically)
Storybook not updating?
# Rebuild the library
npm run build

# Restart Storybook
npm run storybook