Framework
Features

Icons

Drop SVG files into folders and the build turns them into a stylesheet of icon-<name> classes — inlined, tintable with currentColor, and with a preview page to browse them.

The source folder

Icons are plain .svg files grouped in folders, one folder per family. The file name becomes the icon name:

icons/
├── actions/
│   ├── add.svg
│   └── delete.svg
└── social/
    ├── facebook.svg
    └── x.svg

Registering an icon set

Point the builder at that directory and register one or more icon sets in a config file. A set has a name, the folders it pulls from, and where its stylesheet is written — so an app can ship one bundle for the admin and a smaller one for the public site:

config/Icons.config.php
use Framework\Core\Icons;

Icons::setSource("icons");
Icons::setPreview("public/icons.html");
Icons::setMapping("icons/icons.jsonc");
Icons::setTitle("ACME");

Icons::register("admin", [ "actions", "social" ], "public/css/icons.css");
Icons::register("site",  [ "social" ],            "public/css/site-icons.css");
MethodSets
setSource(path)The directory holding the icon folders. Required.
register(name, folders, stylePath)An icon set: which folders go into which stylesheet.
setPreview(path)Optional HTML page listing every icon.
setMapping(path)Optional JSON file describing where each icon came from.
setTitle(title)Optional heading for the preview page, for a name the package cannot spell.

Generating

./framework icons

The command reads each folder, builds a stylesheet per registered set and writes the preview page, reporting what it found. Nothing is generated if no source directory or no set is registered.

The preview page

When you set a setPreview() path, the build also writes a self-contained HTML page — a small browser for your whole icon library. It has no dependencies and inlines its own styles and script, so you can open the file directly or serve it with the app:

Icons::setPreview("public/icons.html");

The page renders every icon grouped by folder, and is built for finding one quickly:

It offersSo you can
A search boxFilter by name, source or Material Symbol name as you type.
Folder navigationJump to a folder, each showing its icon count.
Source filtersShow only the google, fontAwesome or custom icons.
Light & dark modesCheck the icons against both themes — the choice is remembered.
A card per iconSee the icon rendered large, with its class name and source badge.

The extra detail comes from the mapping file: with it in place each card shows where the icon came from, and a google icon links straight to its page on Google Fonts for re-exporting.

Naming the page

The heading is the vendor half of your composer.json name, with the dashes turned into spaces and each word capitalised — acme-tools/backend becomes Acme Tools. Composer requires that name to be lowercase, so an acronym comes back title cased and there is no spelling of it that survives:

"name": "acme/backend"   // the page reads Acme

setTitle() is the way out. It is optional, and a project that does not set one keeps the name derived from the package:

Icons::setTitle("ACME");

The generated CSS

Each icon is inlined as a data url on a custom property, and a single base rule renders it as a CSS mask — which is what lets an icon take its color from the surrounding text:

[class^="icon-"]:before,
[class*=" icon-"]:before {
    content: "";
    display: inline-block;
    width: 1em;
    height: 1em;
    background-color: currentColor;
    mask: var(--icon) no-repeat center / contain;
}

.icon-add:before    { --icon: url("data:image/svg+xml,…"); }
.icon-delete:before { --icon: url("data:image/svg+xml,…"); }

Use it by class — the icon sizes with the font (1em) and inherits the text color, so no fill or size attributes are needed:

<i class="icon-add"></i>
<button class="icon-delete">Delete</button>

The mapping file

The optional mapping records where each icon came from, so a set stays maintainable as it grows — you can tell at a glance which icons are Material Symbols, which are Font Awesome and which were drawn by hand. It is reference only: nothing in the generated CSS depends on it, and an icon with no entry still works. Its one job is to give the preview page something to show and something to filter by.

It is keyed by folder and then by icon — the folder as it sits under the source directory, the icon as its file name without the .svg. Every value is one string.

// comments are stripped before it is read, which is why it is named .jsonc rather than .json — the syntax is worth carrying at the top of the file. That header earns its keep beyond the next person: it is what an assistant asked to add an icon reads to learn which set the ones around it came from and under what name, so the new one is exported to match instead of guessed at. It is validated in your editor by the icons JSON schema:

icons/icons.jsonc
// Where each icon came from, so it can be found and re-exported later.
//
//   "<icon>": "<icon set> <name in that set> <properties…>"
//
//   icon set     google, fontAwesome, lucide, custom — whatever you call it
//   name         what it is called there, which is rarely the file name
//   properties   fill, w100 to w700, s20 s24 s40 s48, or a word of your own

{
    "actions": {
        "add":    "google add_circle fill",
        "delete": "google delete w300 s24",
        "pin":    "lucide map-pin"
    },
    "social": {
        "facebook": "fontAwesome facebook-f",
        "x":        "custom"
    }
}

The syntax of a value

The value is read as three space-separated parts, and every one of them may be left off:

"<icon set> <name in that set> <properties…>"
PartIs
The icon setThe first word — where the icon came from. It can be anything: google, fontAwesome, lucide, custom, the name of a designer. It becomes the badge on the card and one of the Sources filters, counted across the whole library.
The name in that setThe second word — what the icon is called where it came from, which is rarely what you called the file. It is searchable, so add_circle finds your add.
The propertiesEverything after it, split on spaces. Each one becomes a Variants filter, again counted across the library — which is how you find every icon still exported at the wrong weight.

So "google add_circle fill" is the Material Symbol add_circle, exported filled; "custom" is an icon of your own with nothing more to say about it; and an icon absent from the file has no badge, no tags and is filtered out by every one of them.

The properties

A property is only a word, and the page shows any of them. Three are given a friendlier title, because they are the ones a Material Symbols export produces:

WrittenShown asIs
fillFilledThe filled variant rather than the outlined one.
w300, w400…Weight 300The stroke weight it was exported at. Any w followed by digits.
s20, s24…Size 20The optical size it was drawn for. Any s followed by digits.
anything elseCapitalisedduotone shows as Duotone. Nothing is rejected.

One set is special: a google entry that also names its symbol gets a link on its card straight to that icon on Google Fonts, for when you need to re-export it at another weight. Every other set is a label.