Skip to main content

dioxus_icon_component/
lib.rs

1// Copyright (c) 2026 Xu Shaohua <shaohua@biofan.org>. All rights reserved.
2// Use of this source is governed by Apache-2.0 License
3// that can be found in the LICENSE file.
4
5use dioxus::prelude::*;
6
7/// This trait is used to override `IconProps`.
8///
9/// Implements this trait when adding a new real icon.
10pub trait IconShape: Clone + PartialEq + 'static {
11    /// Returns the SVG child elements (paths, circles, etc.) that define the icon shape.
12    ///
13    /// # Errors
14    ///
15    /// Returns `Err` if rendering the child elements fails.
16    fn child_elements(&self) -> Element;
17
18    /// Default title text for the SVG element.
19    const TITLE: Option<&'static str> = None;
20
21    /// Default width of the SVG element in pixels.
22    const WIDTH: Option<&'static str> = None;
23
24    /// Default height of the SVG element in pixels.
25    const HEIGHT: Option<&'static str> = None;
26
27    /// Default fill color of the SVG element.
28    const FILL: Option<&'static str> = Some("currentColor");
29
30    /// Default stroke color of the SVG element.
31    const STROKE: Option<&'static str> = None;
32
33    /// Default stroke width of the SVG element (e.g., "2").
34    const STROKE_WIDTH: Option<&'static str> = None;
35
36    /// Default stroke line cap style (e.g., "round", "butt", "square").
37    const STROKE_LINE_CAP: Option<&'static str> = None;
38
39    /// Default stroke line join style (e.g., "round", "miter", "bevel").
40    const STROKE_LINE_JOIN: Option<&'static str> = None;
41
42    /// Default view box string (e.g., "0 0 24 24").
43    const VIEW_BOX: Option<&'static str> = None;
44
45    /// Default XML namespace for the SVG element.
46    /// Falls back to `"http://www.w3.org/2000/svg"` if not set.
47    const XMLNS: Option<&'static str> = Some("http://www.w3.org/2000/svg");
48}
49
50/// Props for the `Icon` component.
51///
52/// All fields are optional except `icon`. When a field is not provided,
53/// the default value from the associated `IconShape` implementation is used.
54#[derive(Clone, PartialEq, Props)]
55pub struct IconProps<T: IconShape> {
56    /// The icon shape implementation that provides SVG child elements.
57    pub icon: T,
58
59    /// Optional title text rendered as a `<title>` element inside the SVG for accessibility.
60    #[props(default = None)]
61    pub title: Option<&'static str>,
62
63    /// Default width of the SVG element in pixels.
64    #[props(default = None)]
65    pub width: Option<&'static str>,
66
67    /// Default height of the SVG element in pixels.
68    #[props(default = None)]
69    pub height: Option<&'static str>,
70
71    /// Default fill color of the SVG element.
72    #[props(default = None)]
73    pub fill: Option<&'static str>,
74
75    /// Default stroke color of the SVG element.
76    #[props(default = None)]
77    pub stroke: Option<&'static str>,
78
79    /// Default stroke width of the SVG element (e.g., "2").
80    #[props(default = None)]
81    pub stroke_width: Option<&'static str>,
82
83    /// Default stroke line cap style (e.g., "round", "butt", "square").
84    #[props(default = None)]
85    pub stroke_line_cap: Option<&'static str>,
86
87    /// Default stroke line join style (e.g., "round", "miter", "bevel").
88    #[props(default = None)]
89    pub stroke_line_join: Option<&'static str>,
90
91    /// Additional HTML/SVG global attributes to spread onto the `<svg>` element.
92    /// Allows passing standard attributes like `id`, `aria-*`, event handlers, etc.
93    #[props(extends = GlobalAttributes)]
94    pub attributes: Vec<Attribute>,
95}
96
97/// # Errors
98///
99/// Returns `Err` if rendering the icon fails.
100#[allow(non_snake_case)]
101pub fn Icon<T: IconShape>(props: IconProps<T>) -> Element {
102    rsx! {
103        svg {
104            width: if props.width.is_some() {
105                props.width
106            } else {
107                T::WIDTH
108            },
109            height: if props.height.is_some() {
110                props.height
111            } else {
112                T::HEIGHT
113            },
114            fill: if props.fill.is_some() {
115                props.fill
116            } else {
117                T::FILL
118            },
119            stroke: if props.stroke.is_some() {
120                props.stroke
121            } else {
122                T::STROKE
123            },
124            stroke_width: if props.stroke_width.is_some() {
125                props.stroke_width
126            } else {
127                T::STROKE_WIDTH
128            },
129            stroke_linecap: if props.stroke_line_cap.is_some() {
130                props.stroke_line_cap
131            } else {
132                T::STROKE_LINE_CAP
133            },
134            stroke_linejoin: if props.stroke_line_join.is_some() {
135                props.stroke_line_join
136            } else {
137                T::STROKE_LINE_JOIN
138            },
139            view_box: T::VIEW_BOX,
140            xmlns: T::XMLNS,
141
142            ..props.attributes,
143
144            if let Some(title_text) = props.title {
145                title {{ title_text }}
146            } else if let Some(title_text) = T::TITLE {
147                title {{ title_text }}
148            }
149
150            {props.icon.child_elements()}
151        }
152    }
153}