Icon Picker

A control for choosing one of your registered icons, which posts the icon's name

<sa-icon-picker name="AmenityIcon" value="waves-ladder" label="Amenity icon"
                description="Shown beside the amenity on the hotel page.">
    <sa-icon name="wifi" />
    <sa-icon name="waves-ladder" />
    <sa-icon name="coffee" />
    <sa-icon name="utensils" />
    <sa-icon name="wine" />
    <sa-icon name="square-parking" />
    <sa-icon name="dumbbell" />
    <sa-icon name="bath" />
    <sa-icon name="snowflake" />
    <sa-icon name="tv" />
    <sa-icon name="dog" />
    <sa-icon name="baby" />
    <sa-icon name="bus" />
    <sa-icon name="plane-takeoff" />
    <sa-icon name="bike" />
    <sa-icon name="luggage" />
</sa-icon-picker>

Usage

<sa-icon-picker> edits a string that holds the name of an icon. Bind it with asp-for, or give it a name and value, and list the icons it offers as child <sa-icon> elements. Each child names a registered icon, and only its name is used. Search matches the names and the icons' keywords, which the built-in Lucide and Tabler icons include.

<sa-icon-picker asp-for="...">
    <sa-icon name="..." />
</sa-icon-picker>

The button shows the current icon and its name. It opens a popover with a search box, a grid of the icons and a status line that names the icon under the pointer and counts the matches. The current icon comes first. Choosing an icon closes the popover and puts its name in a hidden input, which is what the form posts. A field that isn't required offers Clear in the status line.

The current icon is rendered on the server, so the value shows before the script runs. A stored name that isn't one of the icons still shows, with a dashed box in place of the icon, and posts unchanged. Check a newly chosen name on the server, since a request can post any value.

Examples

Inline

Set layout to IconPickerLayout.Inline to show every icon as a tile, chosen with one click, without a popover. It suits a short list of a couple of dozen icons at most. The tiles are a single-select toggle group, so they work without the script. An optional field is cleared by clicking the chosen tile again, which needs the script.

<sa-icon-picker name="TripStyleIcon" value="sailboat" layout="IconPickerLayout.Inline" label="Trip style icon">
    <sa-icon name="sailboat" />
    <sa-icon name="mountain" />
    <sa-icon name="tent" />
    <sa-icon name="tree-palm" />
    <sa-icon name="camera" />
    <sa-icon name="map" />
</sa-icon-picker>

Icons from a URL

To offer a large set, such as every registered icon, give icons-url instead of child icons. The picker requests the URL once, when it first opens, and searches and scrolls through the whole set in the browser, rendering more icons as the grid scrolls. A form that is never opened makes no request. icons-url can't be combined with child icons or the inline layout.

<sa-icon-picker name="CategoryIcon" value="compass" icons-url="/IconPicker?handler=Icons" label="Category icon"
                description="Choose from every registered icon." />

The demo loads a static copy of the endpoint's response. See the icons endpoint for what the URL returns.

Model binding

Bound with asp-for, the picker takes its name, value, label and description from the property, and is required when the property is [Required].

<sa-icon-picker asp-for="Icon">
    <sa-icon name="mountain" />
    <sa-icon name="sailboat" />
    <sa-icon name="tent" />
    <sa-icon name="tree-palm" />
    <sa-icon name="bike" />
    <sa-icon name="camera" />
</sa-icon-picker>

Disabled

Set disabled to show the icon without letting the user change it. Like a native control, a disabled picker doesn't post its value.

<sa-icon-picker name="ArchivedIcon" value="ship" label="Archived tour icon" disabled="true">
    <sa-icon name="ship" />
    <sa-icon name="sailboat" />
</sa-icon-picker>

The icons endpoint

The picker sends a GET request to icons-url, with no query string, and expects JSON:

{
  "icons": [
    { "name": "wifi", "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" …>…</svg>", "keywords": ["internet", "wireless"] }
  ]
}

name and svg are required, and keywords is optional. Search matches the names and the keywords. svg is complete markup with an <svg> root, so the icons can come from any source, not only IconOptions. The picker skips an entry whose markup has another root, and removes scripts, foreignObject, event attributes and javascript: links before showing it. If the request fails, the popover says so and the picker tries again the next time it opens.

This endpoint returns every registered icon from IconOptions:

public IActionResult OnGetIcons([FromServices] IOptions<IconOptions> iconOptions)
{
    var icons = iconOptions.Value;
    var payload = icons.GetIconNames().Order(StringComparer.Ordinal).Select(name =>
    {
        icons.TryGetIcon(name, out var icon);
        return new { name, svg = ToSvg(icon!), keywords = icon!.Keywords };
    });

    return new JsonResult(new { icons = payload });
}

private static string ToSvg(IconDefinition icon)
{
    var svg = new StringBuilder("<svg");
    foreach (var (name, value) in icon.Attributes)
    {
        svg.Append($" {name}=\"{WebUtility.HtmlEncode(value)}\"");
    }

    svg.Append('>');
    foreach (var shape in icon.Shapes)
    {
        svg.Append($"<{shape.Name}");
        foreach (var (name, value) in shape.Attributes)
        {
            svg.Append($" {name}=\"{WebUtility.HtmlEncode(value)}\"");
        }

        svg.Append("/>");
    }

    return svg.Append("</svg>").ToString();
}

The whole Lucide set, with keywords, is about 1 MB of JSON, and about 150 KB compressed, so enable response compression, or serve a precompressed copy, and let browsers cache the response. Put a version in the URL, such as a hash of the icons, when the set can change.

Accessibility

The button announces the current icon's name and opens a dialog popover. In the popover, the search box has focus first. ↓ moves from the search box into the grid, the arrow keys move between icons, Enter chooses the focused icon and ↑ from the first row returns to the search box. In the search box, Escape first clears the search and then closes the popover. The status line is a polite live region that announces the number of matches. Focus returns to the button when the popover closes.

aria-label and aria-describedby on <sa-icon-picker> are passed to the button, or to the toggle group with the inline layout. Inline tiles are named by their icon's name.

Part classes

Pass an IconPickerClassNames instance from the StellarAdmin.UI namespace using class-names. Each property accepts an optional string of space-separated CSS classes, which are added to the part's existing classes.

PropertyTarget
RootThe generated field wrapper, when the control renders an implicit field.
LabelThe generated field label, when rendered.
DescriptionThe generated help text, when rendered.
ErrorThe generated validation message element, when rendered.
ContentThe generated container around the label and supporting text in a horizontal implicit field.
ControlThe picker's container.
TriggerThe button that opens the popover.
PopoverThe popover.
ItemEach tile of an inline picker.

Root, Label, Description, Error, and Content are inherited from FieldClassNames; see Field part classes.

API Reference

<sa-icon-picker>

Renders a web component containing the button and its popover, or the inline tiles, and a hidden <input> that posts the chosen icon's name.

Prop

Type

The control throws when it has both child icons and icons-url, when it has neither, when the inline layout has icons-url, and when a child <sa-icon> has no name.

On this page