DEV Community

Cover image for I built Material Symbols SVG, an icon library for using Material Symbols as SVG components
k-s-h-r
k-s-h-r

Posted on

I built Material Symbols SVG, an icon library for using Material Symbols as SVG components

I built Material Symbols SVG, an icon library that lets you use Google's Material Symbols as SVG components across frameworks.

It currently supports:

  • React: @material-symbols-svg/react
  • Vue: @material-symbols-svg/vue
  • Svelte: @material-symbols-svg/svelte
  • Astro: @material-symbols-svg/astro
  • React Native: @material-symbols-svg/react-native

Links:

Why I built it

There is already an official-ish way to consume Material Symbols as SVG files, for example @material-symbols/svg-400.

The problem I kept running into was this: if I wanted to use those icons in React as components, I usually had to rely on another layer such as @svgr/webpack, or build some custom conversion flow around raw SVG files.

That works, but it is not what I wanted for day-to-day UI work.

What I wanted instead was:

  • no extra SVG conversion step
  • no framework-specific webpack setup just to render icons
  • direct imports that already behave like components
  • a consistent mental model across frameworks

So I built a library that ships Material Symbols as framework components from the start.

What the library provides

At the moment, the library includes:

  • 3,800+ icons
  • 3 style variants: outlined, rounded, sharp
  • 7 weight variants: 100 through 700
  • filled variants
  • framework packages with similar import patterns

Because these are distributed as SVG components rather than icon fonts, they fit nicely into regular component-based UI code. You can control them with things like size, color, className or class, style, and standard SVG attributes.

Basic usage

Here is a React example:

import { Home, Search } from "@material-symbols-svg/react";
import { Home as HomeRounded } from "@material-symbols-svg/react/rounded";
import { Home as HomeW500 } from "@material-symbols-svg/react/w500";

export function Example() {
  return (
    <div style={{ display: "flex", gap: 12, alignItems: "center" }}>
      <Home />
      <Search size={20} color="#64748b" />
      <HomeRounded size={28} />
      <HomeW500 size={28} color="#0ea5e9" />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The default import is outlined / w400.

import { Home } from "@material-symbols-svg/react"; // outlined / w400
import { Home as HomeW600 } from "@material-symbols-svg/react/w600";
import { Home as HomeRounded } from "@material-symbols-svg/react/rounded";
import { Home as HomeSharpW700 } from "@material-symbols-svg/react/sharp/w700";
Enter fullscreen mode Exit fullscreen mode

The same import-path idea is used across the other framework packages too.

Why style and weight are split by import path

One of the main design choices in this library is that style and weight are selected by import path, not by runtime props.

That is not just an API preference. It comes from how Material Symbols SVG data is structured.

Material Symbols are not simple line icons where you can change stroke width at runtime and get a new weight. Each style and weight combination has its own filled path data.

That means:

  • outlined, rounded, and sharp are different SVG paths
  • w100 through w700 are also different SVG paths

So if I designed the API like this:

<Home styleVariant="rounded" weight={600} />
Enter fullscreen mode Exit fullscreen mode

then a single icon component would tend to carry up to 3 x 7 = 21 path variants inside one module.

That is convenient at the call site, but it is not great for module boundaries and tree-shaking. Unused variants are much more likely to sit inside the same import graph.

By splitting variants into import paths like:

  • @material-symbols-svg/react/w600
  • @material-symbols-svg/react/rounded/w400
  • @material-symbols-svg/react/sharp/w700

the bundler has a much clearer module shape to work with.

Deep imports for individual icons

The library also supports deep imports through icons/*.

import { HomeW400, HomeFillW600 } from "@material-symbols-svg/react/icons/home";
import { SettingsW400 } from "@material-symbols-svg/react/rounded/icons/settings";
Enter fullscreen mode Exit fullscreen mode

This helps for a few reasons:

  • the exact icon is explicit in the import path
  • filled variants are easy to target
  • in some setups, dev server startup and HMR behave better than broad root imports

This part became especially important when I tested framework-specific developer experience.

For example, in Astro 5 I hit cases where root imports timed out during dev startup, while icons/* deep imports worked reliably. So for some frameworks, deep imports are not just a nice extra API, but a practical escape hatch for development performance.

Standard SVG props and accessibility

The components accept standard SVG-oriented props and attributes such as:

  • size
  • color
  • className / class
  • style
  • aria-*
  • data-*

Example:

import { Home } from "@material-symbols-svg/react";

export function Example() {
  return (
    <Home
      size={32}
      color="#ff6b6b"
      className="drop-shadow-lg"
      aria-label="Home"
      data-testid="home-icon"
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

You can also pass a <title> as a child:

<Home>
  <title>Home</title>
</Home>
Enter fullscreen mode Exit fullscreen mode

which ends up inside the rendered SVG:

<svg ...>
  <path ...></path>
  <title>Home</title>
</svg>
Enter fullscreen mode Exit fullscreen mode

That keeps accessibility metadata close to the icon component itself.

Tailwind CSS works well too, because you can style the icon just like any other component:

import { Search } from "@material-symbols-svg/react";

export function SearchButton() {
  return (
    <button className="inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm hover:bg-slate-50">
      <Search className="size-5 text-slate-600" />
      Search
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Next.js optimization

For the React package, Next.js users can also improve dev-time behavior with optimizePackageImports:

const nextConfig = {
  experimental: {
    optimizePackageImports: [
      "@material-symbols-svg/react",
      "@material-symbols-svg/react/rounded",
      "@material-symbols-svg/react/sharp",
    ],
  },
};

export default nextConfig;
Enter fullscreen mode Exit fullscreen mode

Official docs:

https://nextjs.org/docs/app/api-reference/config/next-config-js/optimizePackageImports

Next.js already optimizes some libraries by default, such as lucide-react and react-icons/*, but @material-symbols-svg/react is not included in that default list, so it needs to be configured explicitly.

I also built an icon catalog site

Along with the usage docs, I built a site where you can browse the icons and inspect variants:

https://www.material-symbols-svg.com/

It lets you:

  • search by icon name
  • compare style, fill, weight, size, and color
  • check framework-specific import snippets
  • copy the SVG directly

When you are dealing with thousands of icons, that kind of browsing tool matters more than I expected. It is useful not just for common UI icons, but also for the weird niche ones.

One thing I really like about Material Symbols is that the set is huge enough that you keep finding icons you did not expect to exist. There are very specific icons in there, including things like vacuum-related icons. My personal favorite is still owl.

Closing

I built this because I wanted Material Symbols to feel like first-class components instead of raw SVG assets that need another toolchain step.

If that sounds useful, take a look here:

If you try it and have feedback, I would love to hear it.

Top comments (0)