DocsThe ecosystem
Icons
An optional package that installs icons into your application as ordinary Rahti components. An icon is fetched once and written into src/components/, so what compiles into the binary is the icons the application actually uses — not a crate of sixteen hundred.
Install
Two halves of one release line, and both are needed: rahti-icons is the runtime the generated files call into, and cargo-rahti-icons is the tool that writes them. They ship on the same version, because a file written by one version of the installer is read by the runtime of the same version.
cargo add rahti-icons # the runtime the generated files call
cargo install cargo-rahti-icons # the installer that writes them
cargo rahti-icons add useruser becomes src/components/rahti_icons/user.rs, holding a User component. The Rahti build already scans src/components/, so the icon is in scope on the next build with nothing to wire up.
use crate::components::rahti_icons::user::User;
html! {
<button class="inline-flex items-center gap-2 rounded-md px-3 py-2">
<User class="size-4" />
"Profile"
</button>
}What lands in the repository
Six lines and an SVG path, which is the entire point of the design. Everything that decides how an icon behaves lives in the runtime crate, so upgrading rahti-icons changes the behaviour of every installed icon without re-fetching one of them.
// @generated by cargo-rahti-icons 0.0.1 — do not edit.
//
// The `user` icon, fetched from the ppicons catalog.
// Re-fetch it with `cargo rahti-icons update`; delete this file to remove it.
//
// Props: class, size, width, height, stroke_width, color, fill, label, attrs —
// plus any `{…}` binding written on the tag. See the `rahti-icons` crate.
::rahti_icons::icon! {
/// The `user` icon.
User {
name: "user",
attrs: [
("xmlns", "http://www.w3.org/2000/svg"),
("width", "24"),
("height", "24"),
("viewBox", "0 0 24 24"),
("fill", "none"),
("stroke", "currentColor"),
("stroke-width", "2"),
("class", "lucide lucide-user"),
],
body: "<path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/>",
}
}A generated file writes no use. Everything it names is spelled absolutely — ::rahti_icons::icon!, and inside the macro ::rahti::component and ::rahti::html! — so an application's own imports cannot shadow it.
Props
Every prop is optional. html! closes a props literal with ..Default::default(), so a tag names what it changes and nothing else.
| Prop | Does |
|---|---|
| class | Adds classes to the icon's own, after them |
| size | Sets width and height together |
| width, height | Sets one of them, beating size |
| stroke_width | Sets stroke-width |
| color | Paints the icon — stroke or fill, whichever it takes its colour through |
| fill | Sets fill outright |
| label | The accessible name. Without one, an icon is decorative |
| attrs | An Attrs set: any other attribute, and removals |
// Every prop is optional — a bare tag renders the icon as it was drawn.
<User />
// Sizing and colour.
<User size="20" color="#4f46e5" />
// An accessible name turns a decorative icon into content.
<User size="20" label="Your profile" />
// Anything else, and removals, through an attribute set.
<User class="size-4" attrs=@{Attrs::new().set("id", "avatar").unset("stroke-width")} />
// A `{…}` prop is a client binding, and lands on the <svg> itself.
<User class="size-4" onclick={openMenu()} />Values are Rust expressions, so a number is written as a string — size="20", not size=20 — the same as every other Rahti prop that takes a &str. A {…} prop is a client binding and lands on the <svg> itself.
The merge order
Later steps overwrite earlier ones in place, so an attribute the icon already had keeps its position in the tag. This order is the package's contract, and the runtime's tests assert it one step at a time.
- the icon's own attributes, as the catalog drew them;
size, thenwidthandheight— naming one of the latter beats namingsize;stroke_width;color, onto whichever ofstrokeandfillcurrently readscurrentColor— both, if both do. An icon with no paint attribute at all getscolor, which is whatcurrentColorresolves against;fill;class, appended to the icon's own;label, asrole="img"andaria-label;- the
attrsset, setting and removing whatever it names; - the client bindings, which are the browser's and win outright — a
class={…}binding replaces the class rather than joining it; aria-hidden="true"andfocusable="false", but only if nothing above left an accessible name, a role, or anaria-hiddenof its own.
What renders
One <svg> element, and nothing around it. No wrapper and no fragment markers, so button > svg and [&>svg]:size-4 reach the icon.
<svg pp-component="user_b8e3f3fd" xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="lucide lucide-user size-4"
aria-hidden="true" focusable="false">…</svg>The element and its open tag belong to html!: the package decides which attributes an icon carries and hands them over as an attribute set, and the framework writes the tag — names validated, values escaped, a later attribute replacing an earlier one rather than being written beside it.
Commands
cargo rahti-icons add <icon>... # install one or several
cargo rahti-icons add <icon> --force # re-fetch one already installed
cargo rahti-icons add --all # every icon in the catalog
cargo rahti-icons update # re-fetch everything installed
cargo rahti-icons remove <icon>... # delete the file
cargo rahti-icons list --search arrow # what the catalog has
cargo rahti-icons list --installed # what this project has| Flag | What it does |
|---|---|
| -a, --all | Every icon in the catalog. That is over 1,600 files and a long compile — install what a page uses instead. |
| -f, --force | Re-fetch icons that are already installed. Without it they are left alone. |
| --dir <path> | Write somewhere else under src/components/. A path outside it is refused: a component file elsewhere is declared by no generated mod.rs and is callable from nowhere. |
| --installed | List what this project has rather than what the catalog has. |
| --search <text> | Narrow either list. |
Each command that changes what is installed rewrites rahti-icons.json at the project root and .github/instructions/rahti-icons.instructions.md beside it — so whatever reads the project, a person or a coding agent, is told what is there and how to add more. Neither carries a timestamp, so a run that changed nothing is an empty diff.
Where the icons come from
The ppicons catalog, fetched over HTTPS at install time and never at run time. Names are derived from the catalog slug rather than from the name it suggests for a component, which is what keeps two icons from colliding on a name belonging to neither. A module name is always a legal Rust identifier: a leading digit takes an underscore, and a keyword takes an _icon suffix.
Writing an icon by hand
icon! is what a generated file contains, and nothing stops an application from writing one for an SVG of its own — a logo, a mark, a drawing no catalog has. It gets the same props and the same merge.
// `icon!` is not private to the installer. An SVG of your own works the
// same way, and gets the same props.
rahti_icons::icon! {
/// Our logo.
Logo {
name: "logo",
attrs: [
("xmlns", "http://www.w3.org/2000/svg"),
("viewBox", "0 0 32 32"),
("fill", "currentColor"),
],
body: "<path d=\"M16 2 30 30H2Z\"/>",
}
}