Claude-skill-registry icon-system
Implements scalable icon systems with SVG sprites or React/Vue components. Use when setting up icon libraries, creating icon sizing tokens, optimizing SVGs, or building accessible icon buttons.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/icon-system" ~/.claude/skills/majiayu000-claude-skill-registry-icon-system && rm -rf "$T"
manifest:
skills/data/icon-system/SKILL.mdsource content
Icon System
Overview
Implement a scalable icon system with SVG sprites or icon components, proper sizing tokens, consistent stroke widths, and accessibility. Covers both sprite-based and component-based approaches.
When to Use
- Setting up icons for a new design system
- Converting icon fonts to SVG
- Creating an icon component library
- Establishing icon sizing standards
- Making icons accessible
Quick Reference: Approaches
| Approach | Best For | Bundle Size | Styling |
|---|---|---|---|
| SVG Sprite | Large icon sets, caching | One request | CSS limited |
| Inline SVG Components | Tree-shaking, full control | Per-icon | Full CSS |
| Icon Font | Legacy support | One request | Limited |
| External SVG | Simple sites | Per-icon | Limited |
The Process
- Choose approach: Sprite vs components based on project needs
- Define size scale: Icon size tokens (xs, sm, md, lg, xl)
- Establish stroke width: Consistent line weights
- Create component: Wrapper with accessibility
- Build tooling: Optimization, sprite generation
Size Tokens
Icon Size Scale
:root { /* Icon sizes - match common UI patterns */ --icon-size-xs: 0.75rem; /* 12px - inline text */ --icon-size-sm: 1rem; /* 16px - small buttons */ --icon-size-md: 1.25rem; /* 20px - default */ --icon-size-lg: 1.5rem; /* 24px - large buttons */ --icon-size-xl: 2rem; /* 32px - feature icons */ --icon-size-2xl: 2.5rem; /* 40px - hero icons */ --icon-size-3xl: 3rem; /* 48px - illustrations */ }
Tailwind Config
// tailwind.config.js module.exports = { theme: { extend: { width: { 'icon-xs': '0.75rem', 'icon-sm': '1rem', 'icon-md': '1.25rem', 'icon-lg': '1.5rem', 'icon-xl': '2rem', }, height: { 'icon-xs': '0.75rem', 'icon-sm': '1rem', 'icon-md': '1.25rem', 'icon-lg': '1.5rem', 'icon-xl': '2rem', }, }, }, };
JSON Tokens
{ "icon": { "size": { "xs": { "value": "0.75rem", "description": "12px - inline" }, "sm": { "value": "1rem", "description": "16px - small buttons" }, "md": { "value": "1.25rem", "description": "20px - default" }, "lg": { "value": "1.5rem", "description": "24px - large buttons" }, "xl": { "value": "2rem", "description": "32px - feature icons" } }, "stroke": { "thin": { "value": "1" }, "regular": { "value": "1.5" }, "medium": { "value": "2" }, "bold": { "value": "2.5" } } } }
Approach 1: Inline SVG Components (Recommended)
Best for React, Vue, Svelte. Tree-shakeable, full styling control.
React Icon Component
// components/Icon.tsx import { forwardRef, SVGProps } from 'react'; import clsx from 'clsx'; type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; interface IconProps extends SVGProps<SVGSVGElement> { size?: IconSize; label?: string; // Accessible label } const sizeMap: Record<IconSize, string> = { xs: 'w-3 h-3', // 12px sm: 'w-4 h-4', // 16px md: 'w-5 h-5', // 20px lg: 'w-6 h-6', // 24px xl: 'w-8 h-8', // 32px }; export const Icon = forwardRef<SVGSVGElement, IconProps>( ({ size = 'md', label, className, children, ...props }, ref) => { return ( <svg ref={ref} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={clsx(sizeMap[size], className)} aria-hidden={!label} aria-label={label} role={label ? 'img' : 'presentation'} {...props} > {children} </svg> ); } );
Individual Icon Components
// icons/ChevronDown.tsx import { Icon, IconProps } from '../Icon'; export function ChevronDown(props: IconProps) { return ( <Icon {...props}> <polyline points="6 9 12 15 18 9" /> </Icon> ); } // icons/Check.tsx export function Check(props: IconProps) { return ( <Icon {...props}> <polyline points="20 6 9 17 4 12" /> </Icon> ); } // icons/X.tsx export function X(props: IconProps) { return ( <Icon {...props}> <line x1="18" y1="6" x2="6" y2="18" /> <line x1="6" y1="6" x2="18" y2="18" /> </Icon> ); }
Icon Index with Tree-Shaking
// icons/index.ts export { ChevronDown } from './ChevronDown'; export { Check } from './Check'; export { X } from './X'; export { Search } from './Search'; // ... etc // Usage - only imports what's used import { ChevronDown, Check } from '@/icons';
Usage Examples
// Basic usage <ChevronDown /> // With size <Check size="lg" /> // With accessible label (for meaningful icons) <X label="Close dialog" /> // Custom styling <Search className="text-gray-400" size="sm" /> // In a button <button className="flex items-center gap-2"> <PlusIcon size="sm" /> Add item </button>
Approach 2: SVG Sprite
Best for large icon sets where caching matters.
Sprite File Structure
icons/ ├── sprite.svg # Combined sprite ├── build-sprite.js # Build script └── src/ # Source SVGs ├── arrow-up.svg ├── arrow-down.svg └── check.svg
Sprite Format
<!-- icons/sprite.svg --> <svg xmlns="http://www.w3.org/2000/svg" style="display: none;"> <symbol id="icon-arrow-up" viewBox="0 0 24 24"> <polyline points="18 15 12 9 6 15"/> </symbol> <symbol id="icon-arrow-down" viewBox="0 0 24 24"> <polyline points="6 9 12 15 18 9"/> </symbol> <symbol id="icon-check" viewBox="0 0 24 24"> <polyline points="20 6 9 17 4 12"/> </symbol> </svg>
Usage with Sprite
<!-- Inline sprite in HTML (for same-page access) --> <body> <!-- Embed sprite at top of body --> <svg style="display: none;"> <!-- ... symbols ... --> </svg> <!-- Use icons anywhere --> <svg class="icon icon--md" aria-hidden="true"> <use href="#icon-check"/> </svg> </body>
<!-- External sprite file --> <svg class="icon" aria-hidden="true"> <use href="/icons/sprite.svg#icon-check"/> </svg>
Sprite Component (React)
// components/SpriteIcon.tsx interface SpriteIconProps { name: string; size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; label?: string; className?: string; } export function SpriteIcon({ name, size = 'md', label, className }: SpriteIconProps) { return ( <svg className={clsx('icon', `icon--${size}`, className)} aria-hidden={!label} aria-label={label} role={label ? 'img' : 'presentation'} > <use href={`/icons/sprite.svg#icon-${name}`} /> </svg> ); } // Usage <SpriteIcon name="check" size="lg" />
Build Script (Node.js)
// build-sprite.js const fs = require('fs'); const path = require('path'); const { optimize } = require('svgo'); const iconsDir = './icons/src'; const outputFile = './icons/sprite.svg'; const files = fs.readdirSync(iconsDir).filter(f => f.endsWith('.svg')); let symbols = ''; files.forEach(file => { const name = path.basename(file, '.svg'); const content = fs.readFileSync(path.join(iconsDir, file), 'utf8'); // Optimize SVG const result = optimize(content, { plugins: [ 'removeDoctype', 'removeXMLProcInst', 'removeComments', 'removeMetadata', 'removeTitle', 'removeDesc', 'removeUselessDefs', 'removeEditorsNSData', 'removeEmptyAttrs', 'removeHiddenElems', 'removeEmptyText', 'removeEmptyContainers', { name: 'removeAttrs', params: { attrs: ['class', 'style', 'fill', 'stroke'] } }, ], }); // Extract viewBox and inner content const viewBoxMatch = result.data.match(/viewBox="([^"]+)"/); const viewBox = viewBoxMatch ? viewBoxMatch[1] : '0 0 24 24'; const innerContent = result.data.replace(/<svg[^>]*>/, '').replace(/<\/svg>/, ''); symbols += ` <symbol id="icon-${name}" viewBox="${viewBox}">\n ${innerContent}\n </symbol>\n`; }); const sprite = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none">\n${symbols}</svg>`; fs.writeFileSync(outputFile, sprite); console.log(`Generated sprite with ${files.length} icons`);
CSS for Icons
Base Styles
/* Icon base */ .icon { display: inline-block; vertical-align: middle; flex-shrink: 0; width: var(--icon-size-md); height: var(--icon-size-md); stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; fill: none; } /* Sizes */ .icon--xs { width: var(--icon-size-xs); height: var(--icon-size-xs); } .icon--sm { width: var(--icon-size-sm); height: var(--icon-size-sm); } .icon--md { width: var(--icon-size-md); height: var(--icon-size-md); } .icon--lg { width: var(--icon-size-lg); height: var(--icon-size-lg); } .icon--xl { width: var(--icon-size-xl); height: var(--icon-size-xl); } /* Filled variant */ .icon--filled { fill: currentColor; stroke: none; } /* Adjust stroke for different sizes */ .icon--xs, .icon--sm { stroke-width: 2.5; /* Thicker at small sizes */ } .icon--xl { stroke-width: 1.5; /* Thinner at large sizes */ }
Dark Mode
/* Icons generally inherit color, but you can override */ [data-theme="dark"] .icon--subdued { opacity: 0.8; }
Accessibility
Decorative Icons (Most Common)
// Icon next to text - purely decorative <button> <Icon name="plus" aria-hidden="true" /> Add item </button> // Decorative enhancement <span> <Icon name="check" aria-hidden="true" /> Success </span>
Meaningful Icons
// Icon-only button - needs label <button aria-label="Close dialog"> <Icon name="x" aria-hidden="true" /> </button> // Or use icon's label prop <Icon name="warning" label="Warning" /> // Status indicator <Icon name="error" label="Error occurred" role="img" />
Icon Buttons Pattern
// IconButton component interface IconButtonProps { icon: React.ComponentType<IconProps>; label: string; onClick: () => void; } function IconButton({ icon: IconComponent, label, onClick }: IconButtonProps) { return ( <button type="button" onClick={onClick} aria-label={label} className="p-2 rounded hover:bg-gray-100" > <IconComponent aria-hidden="true" /> </button> ); } // Usage <IconButton icon={TrashIcon} label="Delete item" onClick={handleDelete} />
Animation
/* Spin animation for loading */ .icon--spin { animation: icon-spin 1s linear infinite; } @keyframes icon-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } /* Pulse animation */ .icon--pulse { animation: icon-pulse 2s ease-in-out infinite; } @keyframes icon-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } /* Respect reduced motion */ @media (prefers-reduced-motion: reduce) { .icon--spin, .icon--pulse { animation: none; } }
Optimization
SVGO Config
// svgo.config.js module.exports = { plugins: [ 'preset-default', 'removeDimensions', { name: 'removeAttrs', params: { attrs: ['class', 'data-name', 'fill', 'stroke'], }, }, { name: 'addAttributesToSVGElement', params: { attributes: [ { fill: 'none' }, { stroke: 'currentColor' }, { 'stroke-width': '2' }, { 'stroke-linecap': 'round' }, { 'stroke-linejoin': 'round' }, ], }, }, ], };
Vite/Webpack SVG Import
// vite.config.js import svgr from 'vite-plugin-svgr'; export default { plugins: [ svgr({ svgrOptions: { icon: true, svgProps: { fill: 'none', stroke: 'currentColor', }, }, }), ], };
// Usage with SVGR import { ReactComponent as HomeIcon } from './icons/home.svg'; <HomeIcon className="w-5 h-5" />
Icon Libraries Integration
Lucide React
// Already well-structured icons import { Home, Settings, User } from 'lucide-react'; // Wrap with your size system function Icon({ icon: IconComponent, size = 'md' }) { const sizeMap = { sm: 16, md: 20, lg: 24, xl: 32 }; return <IconComponent size={sizeMap[size]} />; }
Heroicons
import { HomeIcon, Cog6ToothIcon } from '@heroicons/react/24/outline'; import { HomeIcon as HomeIconSolid } from '@heroicons/react/24/solid';
Phosphor Icons
import { House, Gear, User } from '@phosphor-icons/react'; <House size={24} weight="regular" /> <Gear size={24} weight="bold" />
File Organization
src/ ├── components/ │ └── Icon/ │ ├── Icon.tsx # Base component │ ├── Icon.css # Styles │ └── index.ts ├── icons/ │ ├── arrows/ │ │ ├── ChevronDown.tsx │ │ ├── ChevronUp.tsx │ │ └── index.ts │ ├── actions/ │ │ ├── Plus.tsx │ │ ├── Minus.tsx │ │ └── index.ts │ ├── status/ │ │ ├── Check.tsx │ │ ├── X.tsx │ │ └── index.ts │ └── index.ts # Re-exports all └── tokens/ └── icons.json # Size tokens
Common Patterns
Icon + Text Alignment
/* Inline with text */ .inline-icon { display: inline-flex; align-items: center; gap: 0.5em; } /* Icon matches text line-height */ .inline-icon svg { height: 1em; width: 1em; }
Button Icon Positions
// Icon left <button className="flex items-center gap-2"> <PlusIcon size="sm" /> Add item </button> // Icon right <button className="flex items-center gap-2"> Continue <ArrowRightIcon size="sm" /> </button> // Icon only <button aria-label="Settings" className="p-2"> <SettingsIcon /> </button>