Lucide
Partas.Solid.Lucide binds Lucide icons. See its docs for usage.
Usage
Every standard icon is a component in the Partas.Solid.Lucide namespace. For Lucide Lab icons, use the Icon component and pass the icon node:
open Partas.Solid.Lucide
Check() // a standard icon
// Icon component for Lucide Lab icons
Icon(iconNode = LucideLab.yinYang)
The binding is generated from the React library, with a generator written for Feliz. The Solid library is organised less cleanly, but the icon names are the same in both.
Without a binding: vanilla lucide on Solid 2
Until the binding is ported, use the framework-free lucide package directly. Each icon is a named export holding an icon node, a plain array that describes the SVG. createElement turns a node into an <svg> element, and takes optional attributes such as width or stroke-width. Bind both with [<Import>], and import only the icons you use: the icons export holds the whole set and defeats tree shaking.
type IconNode = interface end
[<Import("createElement", "lucide")>]
let createLucideIcon (node: IconNode, attrs: obj) : Browser.Types.Element = jsNative
module LucideIcons =
[<Import("Heart", "lucide")>]
let heart: IconNode = jsNative
[<Import("Star", "lucide")>]
let star: IconNode = jsNative
[<Import("Zap", "lucide")>]
let zap: IconNode = jsNative
[<Import("Sparkles", "lucide")>]
let sparkles: IconNode = jsNative
[<Import("Rocket", "lucide")>]
let rocket: IconNode = jsNative
[<Import("Coffee", "lucide")>]
let coffee: IconNode = jsNative
createElement returns a DOM node that Solid does not know about, so give it a place to live with a ref. A ref callback is enough when the icon never changes, as in the buttons below. The large preview changes with the selection, so an effect reads the signals in its compute function and swaps the <svg> in its effect function. Colour needs no effect at all: Lucide strokes with currentColor, so the wrapper's CSS color sets it.
let lucideChoices =
[| "Heart", LucideIcons.heart
"Star", LucideIcons.star
"Zap", LucideIcons.zap
"Sparkles", LucideIcons.sparkles
"Rocket", LucideIcons.rocket
"Coffee", LucideIcons.coffee |]
let lucideColours =
[| "var(--nacara-primary)"; "#f28b5b"; "#b845fc"; "#1d8fe0"; "#16a34a" |]
[<SolidComponent>]
let LucidePicker () =
let picked, setPicked = createSignal lucideChoices[0]
let colour, setColour = createSignal lucideColours[0]
let stroke, setStroke = createSignal 2.0
let mutable stage: Browser.Types.HTMLDivElement = JS.undefined
// Compute reads the signals; the effect swaps the <svg> whenever they change.
createEffect (
(fun (_: (IconNode * float) option) -> snd (picked ()), stroke ()),
fun (node: IconNode, width: float) ->
let attrs = createObj [ "width" ==> 80; "height" ==> 80; "stroke-width" ==> width ]
let svg = createLucideIcon (node, attrs)
stage?replaceChildren (svg) |> ignore
)
div (style = "display: grid; gap: 1rem; width: min(100%, 30rem)") {
div (style = lucideCard) {
div(style = lucideStage (colour ())).ref (stage)
div (style = "display: grid; gap: .75rem") {
code () { $"{fst (picked ())}, stroke-width {stroke ()}" }
div (style = "display: flex; gap: .5rem") {
For.Keyed(each = lucideColours) {
yield fun swatch _ ->
button(
title = swatch,
style = lucideSwatch swatch (swatch = colour ()),
onClick = fun _ -> setColour swatch
)
.attr ("aria-pressed", string (swatch = colour ()))
}
}
input (
type' = "range",
min = 1,
max = 3,
step = "0.25",
value = string (stroke ()),
onInput = fun e -> setStroke (!!e.currentTarget?valueAsNumber)
)
}
}
div (style = "display: flex; flex-wrap: wrap; justify-content: center; gap: .5rem") {
For.Keyed(each = lucideChoices) {
yield fun (name, node) _ ->
// This icon never changes, so a ref callback is all it needs.
button(
class' = lucideButtonClass (name = fst (picked ())),
title = name,
style = lucideIconButton,
onClick = fun _ -> setPicked (name, node)
)
.ref (fun (el: Browser.Types.HTMLButtonElement) ->
let icon = createLucideIcon (node, {| width = 18; height = 18 |})
el.appendChild icon |> ignore)
}
}
}
export const lucideChoices = [["Heart", Heart], ["Star", Star], ["Zap", Zap], ["Sparkles", Sparkles], ["Rocket", Rocket], ["Coffee", Coffee]];
export const lucideColours = ["var(--nacara-primary)", "#f28b5b", "#b845fc", "#1d8fe0", "#16a34a"];
export function LucidePicker() {
const patternInput = createSignal(item(0, lucideChoices));
const picked = patternInput[0];
const patternInput_1 = createSignal(item(0, lucideColours));
const colour = patternInput_1[0];
const patternInput_2 = createSignal(2);
const stroke = patternInput_2[0];
let stage = void 0;
createEffect((_arg) => [picked()[1], stroke()], (tupledArg) => {
const svg = createElement(tupledArg[0], {
width: 80,
height: 80,
"stroke-width": tupledArg[1],
});
return stage.replaceChildren(svg);
});
return <div style="display: grid; gap: 1rem; width: min(100%, 30rem)">
<div style={lucideCard}>
<div style={lucideStage(colour())}
ref={stage} />
<div style="display: grid; gap: .75rem">
<code>
{`${picked()[0]}, stroke-width ${stroke()}`}
</code>
<div style="display: flex; gap: .5rem">
<KeyedFor each={lucideColours}>
{(swatch, _arg_1) => <button title={swatch}
style={lucideSwatch(swatch, swatch === colour())}
onClick={(_arg_2) => {
patternInput_1[1](swatch);
}}
aria-pressed={toString(swatch === colour())} />}
</KeyedFor>
</div>
<input type="range"
min={1}
max={3}
step="0.25"
value={stroke().toString()}
onInput={(e) => {
patternInput_2[1](e.currentTarget.valueAsNumber);
}} />
</div>
</div>
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: .5rem">
<KeyedFor each={lucideChoices}>
{(tupledArg_1, _arg_3) => {
const name = tupledArg_1[0];
const node_1 = tupledArg_1[1];
return <button class={lucideButtonClass(name === picked()[0])}
title={name}
style={lucideIconButton}
onClick={(_arg_4) => {
patternInput[1]([name, node_1]);
}}
ref={(el_1) => {
const icon = createElement(node_1, {
height: 18,
width: 18,
});
return el_1.appendChild(icon);
}} />;
}}
</KeyedFor>
</div>
</div>;
}
