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:
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");| Method | Sets |
|---|---|
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 offers | So you can |
|---|---|
| A search box | Filter by name, source or Material Symbol name as you type. |
| Folder navigation | Jump to a folder, each showing its icon count. |
| Source filters | Show only the google, fontAwesome or custom icons. |
| Light & dark modes | Check the icons against both themes — the choice is remembered. |
| A card per icon | See 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:
// 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…>"
| Part | Is |
|---|---|
| The icon set | The 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 set | The 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 properties | Everything 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:
| Written | Shown as | Is |
|---|---|---|
fill | Filled | The filled variant rather than the outlined one. |
w300, w400… | Weight 300 | The stroke weight it was exported at. Any w followed by digits. |
s20, s24… | Size 20 | The optical size it was drawn for. Any s followed by digits. |
| anything else | Capitalised | duotone 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.