Iconos SVG Personalizados en React
Receta
Crea componentes de ícono SVG reutilizables y accesibles en React. Usa SVG en línea para tener control total, SVGR para importar archivos .svg como componentes, o un sprite SVG para rendimiento óptimo con muchos iconos.
SVG en línea como componente:
// components/icons/check-icon.tsx
interface IconProps {
size?: number;
color?: string;
className?: string;
"aria-label"?: string;
}
export function CheckIcon({ size = 24, color = "currentColor", className, ...props }: IconProps) {
const isDecorative = !props["aria-label"];
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={color}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
role={isDecorative ? undefined : "img"}
aria-hidden={isDecorative ? true : undefined}
aria-label={props["aria-label"]}
>
<polyline points="20 6 9 17 4 12" />
</svg>
);
}Ejemplo Funcional
Una biblioteca de componentes de conjunto de iconos personalizados con API consistente y accesibilidad:
// components/icons/icon.tsx
import { type SVGProps } from "react";
export interface IconProps extends Omit<SVGProps<SVGSVGElement>, "children"> {
size?: number;
label?: string;
}
function createIcon(path: React.ReactNode, displayName: string) {
function Icon({ size = 24, label, className, ...props }: IconProps) {
const isDecorative = !label;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
role={isDecorative ? undefined : "img"}
aria-hidden={isDecorative ? true : undefined}
aria-label={label}
{...props}
>
{path}
</svg>
);
}
Icon.displayName = displayName;
return Icon;
}
// Define iconos
export const ArrowRight = createIcon(
<line x1="5" y1="12" x2="19" y2="12"><polyline points="12 5 19 12 12 19" /></line>,
"ArrowRight"
);
export const Close = createIcon(
<><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></>,
"Close"
);
export const Heart = createIcon(
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />,
"Heart"
);
export const Star = createIcon(
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />,
"Star"
);
export const Search = createIcon(
<><circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" /></>,
"Search"
);
export const Menu = createIcon(
<><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="18" x2="21" y2="18" /></>,
"Menu"
);// app/components/icon-showcase.tsx
"use client";
import { useState } from "react";
import { ArrowRight, Close, Heart, Star, Search, Menu } from "@/components/icons/icon";
import type { IconProps } from "@/components/icons/icon";
const icons = [
{ component: ArrowRight, name: "ArrowRight" },
{ component: Close, name: "Close" },
{ component: Heart, name: "Heart" },
{ component: Star, name: "Star" },
{ component: Search, name: "Search" },
{ component: Menu, name: "Menu" },
];
export function IconShowcase() {
const [iconSize, setIconSize] = useState(24);
const [iconColor, setIconColor] = useState("#374151");
return (
<div className="space-y-6">
<div className="flex items-center gap-6">
<label className="flex items-center gap-2 text-sm">
Tamaño:
<input
type="range"
min={16}
max={48}
value={iconSize}
onChange={(e) => setIconSize(Number(e.target.value))}
className="w-32"
/>
<span className="w-8 text-right">{iconSize}</span>
</label>
<label className="flex items-center gap-2 text-sm">
Color:
<input
type="color"
value={iconColor}
onChange={(e) => setIconColor(e.target.value)}
className="h-8 w-8 cursor-pointer"
/>
</label>
</div>
<div className="grid grid-cols-3 gap-4 sm:grid-cols-6">
{icons.map(({ component: Icon, name }) => (
<div
key={name}
className="flex flex-col items-center gap-2 rounded-lg border border-gray-200 p-4"
>
<Icon size={iconSize} color={iconColor} label={name} />
<span className="text-xs text-gray-500">{name}</span>
</div>
))}
</div>
</div>
);
}Profundización
Cómo Funciona
- SVG en línea renderiza directamente en el DOM, permitiendo control total de CSS y JavaScript sobre stroke, fill, animaciones y estados hover.
- El valor
currentColorhace que los iconos SVG hereden la propiedad CSScolordel elemento padre, permitiendo estilos con clases de color de texto de Tailwind. - El atributo
viewBox="0 0 24 24"define el espacio de coordenadas. Los atributoswidthyheightcontrolan el tamaño renderizado independientemente. - La función factory
createIconmantiene la API consistente en todos los iconos mientras evita código repetitivo. - Para accesibilidad, los iconos son decorativos (
aria-hidden="true") o informativos (role="img"conaria-label).
Variaciones
SVGR para importar archivos .svg como componentes React:
npm install @svgr/webpack// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: [
{
loader: "@svgr/webpack",
options: {
svgoConfig: {
plugins: [{ name: "removeViewBox", active: false }],
},
},
},
],
});
return config;
},
};
export default nextConfig;// Ahora importa SVGs como componentes
import Logo from "@/public/icons/logo.svg";
export function Header() {
return <Logo className="h-8 w-8 text-blue-600" />;
}Enfoque de sprite SVG para muchos iconos:
// public/icons/sprite.svg
// <svg xmlns="http://www.w3.org/2000/svg">
// <symbol id="icon-check" viewBox="0 0 24 24">
// <polyline points="20 6 9 17 4 12" />
// </symbol>
// <symbol id="icon-close" viewBox="0 0 24 24">
// <line x1="18" y1="6" x2="6" y2="18" />
// <line x1="6" y1="6" x2="18" y2="18" />
// </symbol>
// </svg>
interface SpriteIconProps {
name: string;
size?: number;
className?: string;
label?: string;
}
export function SpriteIcon({ name, size = 24, className, label }: SpriteIconProps) {
return (
<svg
width={size}
height={size}
className={className}
role={label ? "img" : undefined}
aria-hidden={label ? undefined : true}
aria-label={label}
>
<use href={`/icons/sprite.svg#icon-${name}`} />
</svg>
);
}
// Uso: <SpriteIcon name="check" size={20} className="text-green-500" />Notas de TypeScript
- Extiende
SVGProps<SVGSVGElement>para permitir todos los atributos SVG nativos en tus componentes de iconos. - Usa la utilidad
Omitpara eliminarchildrende los props SVG ya que el contenido del icono es fijo. - Para SVGR, agrega un archivo de declaración de tipo para que TypeScript entienda las importaciones
.svg.
// types/svg.d.ts
declare module "*.svg" {
import type { FC, SVGProps } from "react";
const content: FC<SVGProps<SVGSVGElement>>;
export default content;
}Trampas
- Los atributos SVG en JSX usan camelCase (
strokeWidth,viewBox,fillRule) en lugar de kebab-case. React advertirá sobre propiedades DOM inválidas de lo contrario. - Siempre preserva el atributo
viewBoxcuando optimices SVGs con SVGO. Eliminarlo rompe el comportamiento de escalado. - Las propiedades
fillystrokepor defecto sonblacken SVG. Establecefill="none"para iconos basados en stroke ostroke="none"para iconos rellenos explícitamente. - SVGR agrega un elemento
titlepor defecto, lo que crea un tooltip al pasar el ratón. Desactívalo contitleProp: falseen la configuración de SVGR si no lo deseas. - Los sprites SVG cargados vía
use hrefno soportan CSScurrentColorpara sprites de diferente origen. El sprite debe estar en el mismo dominio. - Los SVGs en línea aumentan el tamaño del documento HTML. Para páginas con cientos de iconos, considera sprites o una fuente de iconos en su lugar.
Alternativas
| Enfoque | Pros | Contras |
|---|---|---|
| Componentes SVG en línea | Control total, tree-shakeable, estilos CSS | Verboso, aumenta el bundle por icono |
| Importaciones SVGR | Usa archivos .svg directamente, auto-optimización | Requiere configuración webpack, paso de compilación |
| Sprites SVG | Una sola solicitud para todos los iconos, en caché | Sin tree-shaking, gestión manual del sprite |
| Fuentes de iconos | Bundle diminuto, uso CSS familiar | Borroso en tamaños pequeños, estilos limitados |
| Bibliotecas de iconos (Lucide) | Listos para usar, consistentes, bien mantenidos | Dependencia externa, menos control |
Preguntas Frecuentes
¿Cuáles son los tres enfoques principales para usar iconos SVG personalizados en React?
- Componentes SVG en línea -- escribe marcado SVG directamente en un componente React para tener control total.
- SVGR -- importa archivos
.svgcomo componentes React vía un loader webpack. - Sprites SVG -- referencia iconos de un único archivo sprite usando
<use href>.
¿Cómo hace currentColor que los iconos SVG funcionen con las clases de color de texto de Tailwind?
- Establecer
stroke="currentColor"hace que el icono herede la propiedad CSScolordel padre. - Aplica
className="text-blue-500"en el padre o en el icono, y el color del stroke se actualiza automáticamente.
¿Qué hace la función factory createIcon en el ejemplo?
- Envuelve datos de ruta SVG en un componente consistente con props
size,labelyclassName. - Evita repetir código repetitivo de atributos SVG en cada icono.
- Cada icono obtiene un
displayNamepara React DevTools.
¿Cómo manejas la accesibilidad para iconos SVG decorativos vs. informativos?
- Iconos decorativos: establece
aria-hidden="true"y omiterole. - Iconos informativos: establece
role="img"y proporcionaaria-labelcon una descripción. - El ejemplo verifica si se proporciona un prop
labelpara decidir.
Trampa: ¿Qué sucede si eliminas viewBox al optimizar SVGs con SVGO?
- Eliminar
viewBoxrompe el comportamiento de escalado por completo. - El icono no se redimensionará correctamente con cambios en
width/height. - Configura SVGO con
{ name: "removeViewBox", active: false }.
¿Cómo configuras SVGR para importar archivos .svg como componentes React en Next.js?
// next.config.ts
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: [{ loader: "@svgr/webpack" }],
});
return config;
}Luego: import Logo from "@/public/icons/logo.svg";
¿Qué declaración de tipo TypeScript se necesita para las importaciones de archivos .svg con SVGR?
// types/svg.d.ts
declare module "*.svg" {
import type { FC, SVGProps } from "react";
const content: FC<SVGProps<SVGSVGElement>>;
export default content;
}¿Cómo funciona el enfoque de sprite SVG, y cuál es su limitación con currentColor?
- Los iconos se definen como elementos
<symbol>en un único archivo SVG. - Los componentes los referencian con
<use href="/icons/sprite.svg#icon-name">. currentColorno funciona para sprites de diferente origen; el sprite debe estar en el mismo dominio.
Trampa: ¿Cuáles son los valores por defecto para fill y stroke en SVG?
- Ambos por defecto son
black. - Para iconos basados en stroke, establece explícitamente
fill="none". - Para iconos rellenos, establece explícitamente
stroke="none". - Olvidar esto causa rellenos o strokes negros inesperados.
¿Por qué los atributos SVG usan camelCase en JSX?
- React requiere camelCase para propiedades del DOM:
strokeWidthnostroke-width,viewBoxnoviewbox. - Usar kebab-case genera advertencias de React sobre propiedades DOM inválidas.
¿Cuándo debes usar sprites SVG en lugar de componentes SVG en línea?
- Cuando una página usa cientos de iconos, los SVGs en línea aumentan significativamente el tamaño del documento HTML.
- Los sprites cargan todos los iconos en una única solicitud y se guardan en caché por el navegador.
- Compromiso: los sprites no soportan tree-shaking.
¿Cómo tipas un prop de componente de icono que extiende todos los atributos SVG?
export interface IconProps extends Omit<SVGProps<SVGSVGElement>, "children"> {
size?: number;
label?: string;
}Usa Omit para eliminar children ya que el contenido del icono es fijo.