The shared builder and stylesheet for every `@behivetech/icons.*` package: `createIcon`, the factory each icon package builds its component with, and the stylesheet that sizes them. It holds no glyphs; each one is its own package, `@behivetech/icons.<name>`, so an app loads only the icons it renders.

pnpm add @behivetech/icons.base

This is the snapshot of v0.4.0, the current version, as recorded on 2026-11-10. The live page runs its examples →

registry/behivetech/icons/base at v0.4.0, snapshotted 2026-11-10

create-icon.tsx
1import { type ReactNode, type Ref, type SVGAttributes } from "react";
2import { getClassName } from "@behivetech/get-class-name";
3import styles from "./icon.module.scss";
4
5export type IconSize = "sm" | "md" | "lg" | "xl";
6
7export interface IconBaseProps extends SVGAttributes<SVGSVGElement> {
8 /** Controls width/height; defaults to md */
9 size?: IconSize;
10 /** Additional class names to merge with the component root element */
11 className?: string;
12 /**
13 * Accessible name of the icon, rendered as the svg's `<title>`. When set
14 * the icon is exposed as an image with this name; without it (and without
15 * `aria-label` or `aria-labelledby`) the icon is decorative and hidden from
16 * assistive technology. Pass an already translated string.
17 */
18 titleAccess?: string;
19 /** Ref to the root svg element */
20 ref?: Ref<SVGSVGElement>;
21}
22
23export type IconComponent = (props: IconBaseProps) => ReactNode;
24
25/**
26 * Builds an icon component from its glyph geometry, the way MUI's
27 * `createSvgIcon` does. Each icon package (`@scope/icons.<name>`) calls this
28 * once, and lists this package as a dependency (`workspace:^`), so an app has one
29 * factory and one stylesheet however many icons it uses. Mark each call `PURE`
30 * (a `#__PURE__` comment) so a bundler can drop an icon nothing uses.
31 *
32 * @param displayName The component's name in devtools, e.g. `CheckIcon`
33 * @param children The svg's geometry: one element or a fragment of them, on a 24 by 24 grid
34 * @returns A component taking `IconBaseProps`
35 */
36export function createIcon(
37 displayName: string,
38 children: ReactNode,
39): IconComponent {
40 // React 19 passes `ref` to a function component as an ordinary prop, so
41 // there is no `forwardRef` wrapper here; the ref lands on the root svg. Nor is
42 // there a `memo`: the glyph is one element built once, and re-rendering an
43 // svg of a few nodes costs less than comparing its props.
44 const Component = ({
45 size = "md",
46 className,
47 titleAccess,
48 ref,
49 ...rest
50 }: IconBaseProps) => {
51 // `rootClass` is "BHT__Icon", not "BHT__CreateIcon": this file is the
52 // factory, not a component, and every glyph it builds renders as an Icon.
53 // A conventions audit keying off the file name will flag this — it is
54 // correct as is.
55 const [rootClass] = getClassName({
56 className,
57 rootClass: "BHT__Icon",
58 modifiers: { [size]: true },
59 styles,
60 });
61 const isLabelled = Boolean(
62 titleAccess || rest["aria-label"] || rest["aria-labelledby"],
63 );
64
65 // `rest` comes after the defaults so a caller can override any of them
66 // (`strokeWidth`, `viewBox`, `aria-hidden`), as with MUI's SvgIcon.
67 return (
68 <svg
69 ref={ref}
70 viewBox="0 0 24 24"
71 fill="none"
72 stroke="currentColor"
73 strokeWidth={2}
74 strokeLinecap="round"
75 strokeLinejoin="round"
76 focusable="false"
77 aria-hidden={isLabelled ? undefined : true}
78 role={isLabelled ? "img" : undefined}
79 {...rest}
80 className={rootClass}
81 >
82 {children}
83 {titleAccess ? <title>{titleAccess}</title> : null}
84 </svg>
85 );
86 };
87
88 Component.displayName = displayName;
89 return Component;
90}
icon.module.scss
1.BHT__Icon {
2 display: inline-block;
3 flex-shrink: 0;
4
5 &--sm {
6 width: 1rem;
7 height: 1rem;
8 }
9
10 &--md {
11 width: 1.25rem;
12 height: 1.25rem;
13 }
14
15 &--lg {
16 width: 1.5rem;
17 height: 1.5rem;
18 }
19
20 &--xl {
21 width: 2.5rem;
22 height: 2.5rem;
23 }
24}
index.ts
1export { createIcon } from "./create-icon.js";
2export type { IconBaseProps, IconComponent, IconSize } from "./create-icon.js";
icon.example.tsx
1import { createIcon } from "./create-icon.js";
2
3const SquareIcon = createIcon(
4 "SquareIcon",
5 <rect x="4" y="4" width="16" height="16" rx="2" />,
6);
7
8/** An icon built with `createIcon`, at each size. Real icons come from the `@behivetech/icons.*` packages. */
9export const IconSizes = () => (
10 <>
11 <SquareIcon size="sm" />
12 <SquareIcon />
13 <SquareIcon size="lg" />
14 <SquareIcon size="xl" />
15 </>
16);
17
18/** Named with `titleAccess`, so it is announced as an image. */
19export const NamedIcon = () => <SquareIcon titleAccess="Square" />;