Skip to content

Support BigBlueButton icon font glyphs across the component library #86

Description

@Arthurk12

Is your feature request related to a problem? Please describe.

Every icon slot in this library is typed as React.ReactNode, and the built-in defaults are drawn from three unrelated sources — react-icons/md, @mui/icons-material, and hand-written inline SVG:

Component Built-in icon Source
BBButton helperIcon default, feedbackContent default MdSettings, MdCheckCircle
BBBNavigation fallback icon MdExpandCircleDown
BBBHint icon default, close button MdInfo, MdClose
BBBModal close button MdClose
BBBAccordion expand chevron MdExpandMore
BBBSelect dropdown arrow MdExpandMore
BBBCheckbox round variant RadioButtonUnchecked / RadioButtonChecked
BBBSearch search glyph, clear button hand-written inline <svg>

The BigBlueButton client draws its own UI with a different icon set — the bbb-icons webfont. A plugin built with this library therefore renders Material Design glyphs next to BBB glyphs, in the same toolbar, side by side. There is currently no supported way to ask a component for a BBB glyph.

The library has in fact already grown an undocumented accommodation for it. src/components/Button/styles.ts:218 contains:

export const IconWrapper = styled.div`
  ...
  > i {
    font-size: 2rem;
  }
`;

A bare > i element selector only makes sense for icon-font <i> elements — the icon-font path exists in this codebase today, it is just accidental rather than designed.

On top of that, several icons are hardcoded with no prop to override them at all (BBBModal close, BBBAccordion expand, BBBSelect dropdown arrow, BBBSearch search/clear), so even the workaround above cannot reach them.


How BigBlueButton does it today (analysis)

Checked against bigbluebutton/bigbluebutton, bigbluebutton-html5.

The font and its stylesheet are global, and live outside React.

  • public/fonts/BbbIcons/bbb-icons.woff2 and bbb-icons.woff
  • public/stylesheets/bbb-icons.css — 437 lines, 135 .icon-bbb-* rules
  • client/main.html wires both up before the app boots:
<!-- client/main.html:164 -->
<link rel="preload" href="fonts/BbbIcons/bbb-icons.woff2?v=VERSION" as="font" type="font/woff2" crossorigin="anonymous"/>
<!-- client/main.html:174 -->
<link rel="stylesheet" href="stylesheets/bbb-icons.css" crossorigin="anonymous"/>
<!-- client/main.html:182 -->
<style>
  @font-face {
    font-family: 'bbb-icons';
    src: url('fonts/BbbIcons/bbb-icons.woff2?v=VERSION') format('woff2'),
         url('fonts/BbbIcons/bbb-icons.woff?v=VERSION') format('woff');
    font-weight: normal;
    font-style: normal;
  }
</style>

The stylesheet does the styling; the React component only picks a class.

/* public/stylesheets/bbb-icons.css */
[class^="icon-bbb-"], [class*=" icon-bbb-"] {
  font-family: 'bbb-icons' !important;
  display: inline-block;
  font-style: normal;
  font-weight: 400;
  line-height: 1;
  width: 1em;
  text-align: center;
  vertical-align: middle;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
.icon-bbb-thumbs_up:before { content: "\e92d"; }

Two consequences worth keeping: the glyph is sized by font-size (it is 1em wide) and colored by color, exactly like text — so it composes with the flex wrappers this library already uses; and [class^="icon-bbb-"] matches any class with that prefix, which is how plugins extend the set (see below).

The client's own Icon component is a thin class-name builder (imports/ui/components/common/icon/icon-ts/component.tsx):

const Icon: React.FC<IconProps> = ({
  ariaHidden, ariaLabel, className = '',
  prependIconName = 'icon-bbb-', iconName = '', rotate = false,
}) => {
  const isAriaHidden = ariaHidden ?? !ariaLabel;
  return (
    <Styled.Icon
      aria-hidden={isAriaHidden}
      aria-label={isAriaHidden ? undefined : ariaLabel}
      role={isAriaHidden ? undefined : 'img'}
      className={cx(className, [prependIconName, iconName].join(''))}
      $rotate={rotate}
    />
  );
};

Styled.Icon is styled.i with only a $rotate transform. The older component.jsx variant adds a color prop. Note the accessibility default: hidden unless an ariaLabel is given, in which case it becomes role="img".

prependIconName is a real extension point, not decoration. bigbluebutton-plugin-audioplayer/src/main.css ships its own glyphs under the same family and prefix:

@font-face {
  font-family: 'bbb-icons';
  src: url('../assets/audio-player.woff') format('woff');
}
.icon-bbb-audio-player:before { content: "\e820"; }

Any design here must keep a name outside the known set renderable.

The plugin SDK already standardised an icon-name value type. bigbluebutton-html-plugin-sdk/src/extensible-areas/common/icon/types.ts:

export interface PluginIconName { iconName: string; }
export interface PluginIconSvgContent { svgContent: React.SVGProps<SVGSVGElement>; }
export type PluginIconType = string | PluginIconName | PluginIconSvgContent;

and the client normalises it in several places, e.g. imports/ui/components/common/menu/component.jsx:23 and imports/ui/components/common/separator/component.tsx:15-17:

if (typeof icon === 'string') return <Icon iconName={icon} ... />;
if (icon && typeof icon === 'object' && 'iconName' in icon) return <Icon iconName={icon.iconName} ... />;
if (icon && typeof icon === 'object' && 'svgContent' in icon) return <Wrapper>{icon.svgContent}</Wrapper>;

Plugin authors already hold PluginIconType values. If this library speaks the same shape, those values can be forwarded to a BBButton unchanged.

Duplication is a known cost in the monorepo. bbb-learning-dashboard keeps its own copy at src/fonts/BbbIcons/bbb-icons.woff. Adding a third copy that ships inside the client bundle would be a regression, which drives the font delivery decision below.


Describe the solution you'd like

Six parts. Parts 1–3 are the core and are non-breaking; parts 4–6 are the ergonomics and coverage work.

1. A new BBBIcon component

New folder src/components/Icon/, exported as BBBIcon, mirroring the client's contract so that a glyph rendered by this library is indistinguishable from one rendered by BBB itself.

It ships no CSS and loads no font. It only emits the class name and relies on the rules already present in the host document.

export interface IconProps {
  /** Name of the glyph in the BBB icon font, without the `icon-bbb-` prefix. */
  name: BBBIconName | (string & {});

  /** Class prefix prepended to `name`; override it to reach glyphs a plugin added to the font. @default 'icon-bbb-' */
  prefix?: string;

  /** Glyph size, applied as `font-size`; the glyph is 1em wide and square. @default '1em' */
  size?: string;

  /** Glyph color, applied as `color`; inherits from the parent when omitted. */
  color?: string;

  /** Rotates the glyph 180°, mirrored under `dir="rtl"`. @default false */
  rotate?: boolean;

  /** Accessible name; when set, the icon becomes `role="img"` instead of being hidden. */
  ariaLabel?: string;

  /** Forces the icon to be hidden from assistive technology; defaults to `true` unless `ariaLabel` is set. */
  ariaHidden?: boolean;

  /** Extra class names merged with the generated icon class. */
  className?: string;

  /** Value for the `data-test` attribute. */
  dataTest?: string;
}

name is typed as BBBIconName | (string & {}) rather than a closed union: autocomplete lists the 135 known names, while a plugin-provided glyph still type-checks.

2. Generated, typed icon names

scripts/generate-icon-names.ts parses bbb-icons.css from a pinned BBB version and emits src/components/Icon/icon-names.ts:

export const BBB_ICON_NAMES = ['about', 'add', 'alert', /* …135 total */] as const;
export type BBBIconName = typeof BBB_ICON_NAMES[number];

Regenerating is a deliberate, reviewable commit, so the pinned BBB version is visible in the diff and the union never drifts silently.

3. Font assets only outside the BBB client

The constraint: inside the BBB client the library must add nothing — no @font-face, no re-declaration of bbb-icons.css. Redeclaring @font-face for the family bbb-icons would fight the client's own declaration, and shipping the .woff2 in the bundle would be a third copy of a font already served.

Proposal:

  • The same generator vendors bbb-icons.css + bbb-icons.woff2 as a dev-only asset, imported by .storybook/preview.ts. It is not reachable from any component's module graph, so it cannot end up in a consumer's bundle.

  • For apps outside BBB that still want the glyphs, a separate opt-in entry point with the side effect:

    // only for apps that are NOT the BBB client
    import '@bigbluebutton/bbb-ui-components-react/icon-font';

    Added to package.json exports and to webpack.config.babel.js like any other entry, and listed in sideEffects.

  • No component ever imports it. Inside BBB, nobody calls it and nothing is added.

The BBBIcon README documents this explicitly: in a BBB plugin, import nothing — the font is already there.

4. Accept icon names on the existing icon props

A shared internal helper normalises the three accepted shapes, matching what the client already does for PluginIconType:

// src/components/Icon/render-icon.tsx
export type BBBIconProp = React.ReactNode | BBBIconName | { iconName: string };

export const renderIcon = (icon: BBBIconProp): React.ReactNode => {
  if (typeof icon === 'string') return <BBBIcon name={icon} />;
  if (icon && typeof icon === 'object' && 'iconName' in icon) return <BBBIcon name={icon.iconName} />;
  return icon;
};

Every existing icon prop widens from React.ReactNode to BBBIconProp and is passed through renderIcon at render time. <MdSend /> keeps working unchanged.

Breaking-change flag. React.ReactNode already includes string, so today icon="save" renders the literal text save. Treating a bare string as an icon name changes runtime behaviour for anyone who relied on that. It is an unlikely usage — none of these props are documented as accepting text — but it is a real change. Options, to be settled in review: (a) ship the bare-string form and land it on the next major; (b) accept only the { iconName } object form, fully backwards compatible but more verbose and only partially PluginIconType-compatible; (c) ship (b) now and (a) in the next major. Recommendation: (a), since the whole point is letting plugin authors forward PluginIconType values verbatim.

5. Expose the icons that are currently hardcoded

These have no override today. Each gets a BBBIconProp prop whose default is the current Material icon, so behaviour is unchanged when the prop is omitted.

Component New prop Replaces Current default
BBBModal closeIcon component.tsx:70 <MdClose size="1.5rem" />
BBBHint closeIcon component.tsx:58 <MdClose fontSize="1rem" />
BBBAccordion expandIcon component.tsx:39 <MdExpandMore />
BBBSelect dropdownIcon component.tsx:38 MdExpandMore (MUI IconComponent)
BBBSearch searchIcon, clearIcon component.tsx:87-103, :129-145 hand-written inline <svg>

BBBSelect.dropdownIcon needs care: MUI's IconComponent takes a component type, not an element, so the prop has to be adapted rather than forwarded.

BBBCheckbox's RadioButtonUnchecked / RadioButtonChecked are left alone — they are the round variant's control rendering, not a decorative icon slot.

6. Replace the ad-hoc > i rule with a documented sizing contract

src/components/Button/styles.ts:218 currently hardcodes > i { font-size: 2rem; }. With BBBIcon owning size, that rule should become an explicit default that a caller can override, and the same sizing question answered consistently for BBBNavigation, BBBHint, BBBSlider and BBBInput, whose icon wrappers are each sized differently today.

Worth documenting once, in the BBBIcon README: a font glyph inherits font-size and color, so size/color behave like the identically named props of react-icons — the two icon sources stay interchangeable inside the same wrapper.


Describe alternatives you've considered

Do nothing; consumers keep passing <i className="icon-bbb-…" />. Rejected: no type safety, no a11y defaults, no sizing contract, the > i hack stays undocumented, and it cannot reach the hardcoded icons at all.

Convert the font to individual React SVG components (à la react-icons), generated from the font. Best tree-shaking and no global CSS dependency at all. Rejected: the font is not the source of truth for the vectors, the generated output would drift from the client's rendering, and it duplicates in bytes exactly what the client already loads — for the primary consumer (a plugin inside BBB) this is strictly worse.

Always bundle the font and the @font-face in the library. Self-contained and the simplest story for Storybook. Rejected per the constraint above: it duplicates ~30KB already served by the client, declares a competing @font-face for the family bbb-icons, and risks rendering a different font version than the surrounding UI.

Re-export the client's Icon component from the SDK instead. The SDK does not export it, and the library must stay usable outside a plugin context. Keeping a local BBBIcon whose contract matches the client's is the cheaper coupling.


Affected component(s)

BBBIcon (new), BBButton, BBBNavigation, BBBHint, BBBSelect, BBBInput, BBBSlider, BBBModal, BBBAccordion, BBBSearch.

Prop changes, per component

Component Prop Change
BBBIcon — New component
BBButton icon (circle/squared/stacked) React.ReactNode → BBBIconProp
BBButton iconStart, iconEnd (default layout) React.ReactNode → BBBIconProp
BBButton helperIcon (stacked) React.ReactNode → BBBIconProp
BBBNavigation icon React.ReactNode → BBBIconProp
BBBHint icon React.ReactNode → BBBIconProp
BBBHint closeIcon New, defaults to <MdClose fontSize="1rem" />
BBBSelect icon React.ReactNode → BBBIconProp
BBBSelect dropdownIcon New, defaults to MdExpandMore
BBBInput buttonIcon React.ReactNode → BBBIconProp
BBBSlider iconStart, iconEnd React.ReactNode → BBBIconProp
BBBModal closeIcon New, defaults to <MdClose size="1.5rem" />
BBBAccordion expandIcon New, defaults to <MdExpandMore />
BBBSearch searchIcon, clearIcon New, default to the current inline SVGs

Not changed: BBButton.feedbackContent, BBBInput.beforeButton / afterButton / sentFeedbackContent, BBBAccordion.buttonHeader, BBBModal.footerContent — these are general content slots, not icon slots, and already accept a <BBBIcon /> element as-is. BBBCheckbox and BBBToggle are unaffected.


Proposed API / Usage Example

import { BBBIcon, BBButton, BBBHint, BBBModal } from '@bigbluebutton/bbb-ui-components-react';

// 1. Standalone, inside any content slot
<BBBIcon name="thumbs_up" />
<BBBIcon name="presentation" size="1.5rem" ariaLabel="Presentation" />
<BBBIcon name="right_arrow" rotate />

// 2. By name, on an existing icon prop (replaces the raw <i> workaround)
<BBButton layout="circle" size="sm" icon="thumbs_up" onClick={upvote} ariaLabel="Upvote" />

// 3. Forwarding a PluginIconType value straight from the SDK, unchanged
<BBButton layout="squared" icon={pluginItem.icon} onClick={run} ariaLabel={pluginItem.label} />

// 4. Replacing an icon that is hardcoded today
<BBBModal isOpen onRequestClose={close} title="Settings" closeIcon={<BBBIcon name="close" />}>
  ...
</BBBModal>

// 5. Material icons keep working, unchanged
<BBBHint label="Recording started" icon={<MdInfo fontSize="1rem" />} />

// 6. Glyph absent from the core font, added by the plugin's own font file;
//    type-checks thanks to the `string & {}` arm of `name`
<BBBIcon name="audio-player" />

Outside the BBB client only (Storybook, standalone app, tests):

import '@bigbluebutton/bbb-ui-components-react/icon-font';

Additional context

Suggested delivery order. Parts 1–3 are self-contained and unblock consumers immediately (icon={<BBBIcon name="…" />} works on every existing prop without any other change). Parts 4–6 can follow as separate PRs; part 4 carries the breaking-change decision and should be settled before it lands.

Open questions for review

  1. Bare string vs { iconName } only — see the flag under part 4.
  2. Where is the pinned bbb-icons.css read from at generation time: a committed vendored copy, or fetched from a tag of bigbluebutton/bigbluebutton?
  3. Should the generator run in CI to detect drift against a BBB release, or stay fully manual?
  4. BBBSelect.dropdownIcon has to bridge MUI's IconComponent (component type) and this library's element-based icon props — confirm the adapter shape.
  5. Does BBBIcon need the svgContent arm of PluginIconType too, so a plugin can forward any PluginIconType value, not just the name-based ones?

Repository conventions. The new component follows CLAUDE.md: folder src/components/Icon/ with component.tsx, styles.ts, index.ts, types.ts, constants.ts, component.stories.tsx, README.md, assets/example.png; plus entries in src/components/index.ts, the root README.md, webpack.config.babel.js and package.json exports. The Storybook story is the natural place to render the full 135-glyph grid as living documentation of the available names.

Sources consulted

  • bigbluebutton/bigbluebutton-html5: public/stylesheets/bbb-icons.css, public/fonts/BbbIcons/, client/main.html:164-190, imports/ui/components/common/icon/, imports/ui/components/common/menu/component.jsx:23-44, imports/ui/components/common/separator/component.tsx:15-17
  • bigbluebutton-html-plugin-sdk: src/extensible-areas/common/icon/types.ts
  • bigbluebutton-plugin-audioplayer: src/main.css:1-20
  • This repository: src/components/Button/styles.ts:218, and the icon props of every component listed above

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions