Repository: FortAwesome/react-fontawesome

Issue:
Feature request: fa-layers-text support
Request: add ability to layer text on top of icons as in the SVG + JS version of Font Awesome and described in the [FA documentation](https://fontawesome.com/how-to-use/on-the-web/styling/layering).


Hints from issue comments:
@fillon: Is there a workaround solution ?@avshyz: To be honest, using a span with `.fa-layers-text` worked for me.@robmadole: Yep, the workaround for this is just using the class and span manually. We haven't gotten around to adding a component that will do this yet.@Rc85: > Yep, the workaround for this is just using the class and span manually. We haven't gotten around to adding a component that will do this yet.

The problem is that you cannot use the attribute data-fa-transform to transform the text.@bgaynor78: As @Rc85 points out, you _can_ use the workaround for rendering, but if you need to use transforms to alter the text layer, you are out of luck. Would it be possible to add an extra prop, say `isTextLayer` and then you could do something like:

```
<FontAwesomeIcon
  isTextLayer
  transform="shrink-6 right-2 up-2"
>Txt</FontAwesomeIcon>
```
which in theory would output:

`<span class="fa-layers-text" data-fa-transform="shrink-6 right-2 up-2">Txt</span>`

I know that using children in the element declaration is a divergence from the default of using the `icon` prop, but I feel like it's closer to how you'd specify a text layer in the [JS docs](https://fontawesome.com/how-to-use/on-the-web/styling/layering).

Still though, that doesn't answer the question of how to get the transform prop to do what it needs to do. Not sure I have the knowledge to know where to start on that. It's just a proposal at this point. If pointed in the right direction, I could try to make an attempt at a PR.


**EDIT:** Also, as an aside, you could just use plain ol' CSS to mimic what FontAwesome is doing with the transforms to get you around the "no transforms" issue.@barrychapman: Is this on any roadmap to be added? this would be very helpful to have@ZakKa89: Seconded, I would love to add a euro sign instead of a dollar in a lot of icons. @barrychapman: Following up two years later. Is there any plan to be able to power transform text layers?

@charles4221: I will be picking this one up ASAP for our next feature release (`3.1.0`)@charles4221: I know it is over 6 years since this issue was raised, but I have just opened PR #601 to finally close this feature request!

Any followers of this issue are welcome and encouraged to give the PR a review before I go ahead with it.@charles4221: :tada: This issue has been resolved in version 3.1.0 :tada:

The release is available on [GitHub release](https://github.com/FortAwesome/react-fontawesome/releases/tag/v3.1.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:

Solution patch:
diff --git a/package.json b/package.json
index 38db81f..ebba5d0 100644
--- a/package.json
+++ b/package.json
@@ -14,11 +14,11 @@
       "require": "./dist/index.cjs",
       "default": "./dist/index.js"
     },
-    "./components/rsc/CustomPrefixProvider": {
-      "types": "./dist/components/rsc/CustomPrefixProvider.d.ts",
-      "import": "./dist/components/rsc/CustomPrefixProvider.js",
-      "require": "./dist/components/rsc/CustomPrefixProvider.cjs",
-      "default": "./dist/components/rsc/CustomPrefixProvider.js"
+    "./CustomPrefixProvider": {
+      "types": "./dist/CustomPrefixProvider.d.ts",
+      "import": "./dist/CustomPrefixProvider.js",
+      "require": "./dist/CustomPrefixProvider.cjs",
+      "default": "./dist/CustomPrefixProvider.js"
     },
     "./package.json": "./package.json"
   },
@@ -65,6 +65,7 @@
     "test": "jest",
     "test:watch": "jest --watch",
     "test:prepublish": "npm run install.6 && npm run test && npm run install.7 && npm run test",
+    "validate": "npm run lint && npm run format:check && npm run validate-types && npm run test",
     "validate-types": "tsc --noEmit",
     "install.6": "npm --no-save install @fortawesome/fontawesome-svg-core@6.x @fortawesome/free-solid-svg-icons@6.x",
     "install.7": "npm --no-save install @fortawesome/fontawesome-svg-core@7.x @fortawesome/free-solid-svg-icons@7.x",
diff --git a/src/components/FontAwesomeIcon.tsx b/src/components/FontAwesomeIcon.tsx
index d734542..3a7934e 100644
--- a/src/components/FontAwesomeIcon.tsx
+++ b/src/components/FontAwesomeIcon.tsx
@@ -1,11 +1,11 @@
-import React from 'react'
+import React, { RefAttributes, SVGAttributes } from 'react'
 
 import {
   icon as faIcon,
   parse as faParse,
 } from '@fortawesome/fontawesome-svg-core'
 
-import { convert } from '../converter'
+import { makeReactConverter } from '../converter'
 import { useAccessibilityId } from '../hooks/useAccessibilityId'
 import { Logger } from '../logger'
 import { FontAwesomeIconProps } from '../types/icon-props'
@@ -100,7 +100,11 @@ export const FontAwesomeIcon = React.forwardRef<
   }
 
   const { abstract } = renderedIcon
-  const extraProps: Partial<FontAwesomeIconProps> = { ref }
+  const extraProps: Omit<
+    SVGAttributes<SVGSVGElement>,
+    'children' | 'mask' | 'transform'
+  > &
+    RefAttributes<SVGSVGElement> = { ref }
 
   for (const key of typedObjectKeys(allProps)) {
     // Skip default props
@@ -115,9 +119,7 @@ export const FontAwesomeIcon = React.forwardRef<
     extraProps[key] = allProps[key]
   }
 
-  return convertCurry(abstract[0], extraProps)
+  return makeReactConverter(abstract[0], extraProps)
 })
 
 FontAwesomeIcon.displayName = 'FontAwesomeIcon'
-
-const convertCurry = convert.bind(null, React.createElement)
diff --git a/src/components/FontAwesomeLayers.tsx b/src/components/FontAwesomeLayers.tsx
new file mode 100644
index 0000000..16ce1e8
--- /dev/null
+++ b/src/components/FontAwesomeLayers.tsx
@@ -0,0 +1,193 @@
+import React, { useMemo } from 'react'
+
+import {
+  counter as faCounter,
+  text as faText,
+  parse as faParse,
+  Transform,
+  SizeProp,
+} from '@fortawesome/fontawesome-svg-core'
+
+import { makeReactConverter } from '../converter'
+import { CSSVariables } from '../types/css-variables'
+import { LAYER_CLASSES, STYLE_CLASSES } from '../utils/constants'
+import { withPrefix } from '../utils/get-class-list-from-props'
+
+type Attributes = React.HTMLAttributes<HTMLSpanElement>
+
+interface FontAwesomeLayersProps extends Attributes {
+  children: React.ReactNode
+  className?: string | undefined
+  size?: SizeProp | undefined
+}
+
+const DEFAULT_CLASSNAMES = `${LAYER_CLASSES.default} ${STYLE_CLASSES.fixedWidth}`
+
+/**
+ * React Component that allows you to stack multiple Font Awesome icons on top of each other,
+ * or to layer with text or a counter.
+ *
+ * @see https://docs.fontawesome.com/web/style/layer
+ *
+ * @example
+ * ```tsx
+ * import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/react-fontawesome'
+ * import { faBookmark, faCircle, faCheck, faHeart, faMoon, faPlay, faStar, faSun } from '@fortawesome/free-solid-svg-icons'
+ *
+ * // React versions of the examples from the FontAwesome Web Docs
+ * export const Examples = () => (
+ *   <div className="fa-4x">
+ *     <FontAwesomeLayers>
+ *       <FontAwesomeIcon icon={faCircle} color="tomato" />
+ *       <FontAwesomeIcon icon={faCheck} inverse transform="shrink-6" />
+ *     </FontAwesomeLayers>
+ *     <FontAwesomeLayers>
+ *       <FontAwesomeIcon icon={faBookmark} />
+ *       <FontAwesomeIcon icon={faHeart} color="tomato" transform="shrink-10 up-2" />
+ *     </FontAwesomeLayers>
+ *     <FontAwesomeLayers>
+ *       <FontAwesomeIcon icon={faPlay} transform="rotate--90 grow-4" />
+ *       <FontAwesomeIcon icon={faSun} inverse transform="shrink-10 up-2" />
+ *       <FontAwesomeIcon icon={faMoon} inverse transform="shrink-11 down-4.2 left-4" />
+ *       <FontAwesomeIcon icon={faStar} inverse transform="shrink-11 down-4.2 right-4" />
+ *     </FontAwesomeLayers>
+ *   </div>
+ * )
+ * ```
+ *
+ * For examples using Text or Counter components:
+ * @see {@link LayersText}
+ * @see {@link LayersCounter}
+ */
+const FontAwesomeLayers = ({
+  children,
+  className,
+  size,
+  ...attributes
+}: FontAwesomeLayersProps) => {
+  const prefixedDefaultClasses = withPrefix(DEFAULT_CLASSNAMES)
+  const classes = className
+    ? `${prefixedDefaultClasses} ${className}`
+    : prefixedDefaultClasses
+
+  const element = (
+    <span {...attributes} className={classes}>
+      {children}
+    </span>
+  )
+
+  if (size) {
+    return <div className={withPrefix(`fa-${size}`)}>{element}</div>
+  }
+
+  return element
+}
+
+/**
+ * Text component to be used within a `FontAwesomeLayers` component.
+ *
+ * @see https://docs.fontawesome.com/web/style/layer
+ *
+ * @example
+ * ```tsx
+ * import { FontAwesomeLayers, LayersText } from '@fortawesome/react-fontawesome'
+ * import { faCalendar, faCertificate } from '@fortawesome/free-solid-svg-icons'
+ *
+ * // React versions of the examples from the FontAwesome Web Docs
+ * export const Examples = () => (
+ *   <div className="fa-4x">
+ *     <FontAwesomeLayers>
+ *       <FontAwesomeIcon icon={faCalendar} />
+ *       <LayersText
+ *         text="27"
+ *         inverse
+ *         style={{ fontWeight: '900' }}
+ *         transform="shrink-8 down-3"
+ *       />
+ *     </FontAwesomeLayers>
+ *     <FontAwesomeLayers>
+ *       <FontAwesomeIcon icon={faCertificate} />
+ *       <LayersText
+ *         text="NEW"
+ *         inverse
+ *         style={{ fontWeight: '900' }}
+ *         transform="shrink-11.5 rotate--30"
+ *       />
+ *     </FontAwesomeLayers>
+ *   </div>
+ * )
+ * ```
+ */
+const LayersText = ({
+  text,
+  className,
+  inverse,
+  transform,
+  style,
+  ...attributes
+}: Attributes & {
+  text: string
+  className?: string | undefined
+  inverse?: boolean | undefined
+  transform?: string | Transform | undefined
+  style?: (React.CSSProperties & CSSVariables) | undefined
+}) => {
+  const textAbstractElement = useMemo(() => {
+    const textObject = faText(text, {
+      classes: [
+        ...(className?.split(' ') || []),
+        ...(inverse ? [STYLE_CLASSES.inverse] : []),
+      ],
+      transform:
+        typeof transform === 'string'
+          ? faParse.transform(transform)
+          : transform,
+    })
+
+    return textObject.abstract[0]
+  }, [text, transform, className, inverse])
+
+  return makeReactConverter(textAbstractElement, { ...attributes, style })
+}
+
+/**
+ * Counter component to be used within a `FontAwesomeLayers` component.
+ *
+ * @see https://docs.fontawesome.com/web/style/layer
+ *
+ * @example
+ * ```tsx
+ * import { FontAwesomeLayers, LayersCounter } from '@fortawesome/react-fontawesome'
+ * import { faEnvelope } from '@fortawesome/free-solid-svg-icons'
+ *
+ * // React version of the example from the FontAwesome Web Docs
+ * export const Example = ({ count = 1419 }) => (
+ *   <FontAwesomeLayers size="4x">
+ *     <FontAwesomeIcon icon={faEnvelope} />
+ *     <LayersCounter count={count.toLocaleString()} style={{ backgroundColor: 'tomato' }} />
+ *   </FontAwesomeLayers>
+ * )
+ * ```
+ */
+const LayersCounter = ({
+  count,
+  className,
+  style,
+  ...attributes
+}: Attributes & {
+  count: number | string
+  className?: string | undefined
+  style?: (React.CSSProperties & CSSVariables) | undefined
+}) => {
+  const counterAbstractElement = useMemo(
+    () =>
+      faCounter(count, {
+        classes: className?.split(' '),
+      }).abstract[0],
+    [count, className],
+  )
+
+  return makeReactConverter(counterAbstractElement, { ...attributes, style })
+}
+
+export { FontAwesomeLayers, LayersText, LayersCounter }
diff --git a/src/components/rsc/CustomPrefixProvider.tsx b/src/components/rsc/CustomPrefixProvider.tsx
index 104732b..177ba08 100644
--- a/src/components/rsc/CustomPrefixProvider.tsx
+++ b/src/components/rsc/CustomPrefixProvider.tsx
@@ -22,7 +22,7 @@ interface CustomPrefixProviderProps {
  * // In Next.js App Router - `app/layout.tsx`
  * import '@fortawesome/fontawesome-svg-core/styles.css'
  * import { config } from '@fortawesome/fontawesome-svg-core'
- * import { CustomPrefixProvider } from '@fortawesome/react-fontawesome/components/rsc/CustomPrefixProvider'
+ * import { CustomPrefixProvider } from '@fortawesome/react-fontawesome/CustomPrefixProvider'
  *
  * const CUSTOM_FA_CSS_PREFIX = 'my-custom-prefix'
  *
diff --git a/src/converter.ts b/src/converter.ts
index 8037820..85f8944 100644
--- a/src/converter.ts
+++ b/src/converter.ts
@@ -1,8 +1,12 @@
-import React, { type CSSProperties } from 'react'
+import React, {
+  HTMLAttributes,
+  RefAttributes,
+  SVGAttributes,
+  type CSSProperties,
+} from 'react'
 
 import type { AbstractElement } from '@fortawesome/fontawesome-svg-core'
 
-import type { FontAwesomeIconProps } from './types/icon-props'
 import { camelize } from './utils/camelize'
 
 function capitalize(val: string): string {
@@ -68,12 +72,15 @@ type AttributesOverride = Record<string, unknown> & {
   style?: React.CSSProperties
 }
 
-export function convert(
+export function convert<
+  El extends Element = SVGSVGElement,
+  Attr extends HTMLAttributes<El> = SVGAttributes<El>,
+>(
   createElement: typeof React.createElement,
   element: Omit<AbstractElement, 'attributes'> & {
     attributes: AttributesOverride
   },
-  extraProps: Partial<FontAwesomeIconProps> = {},
+  extraProps: Attr & RefAttributes<El> = {} as Attr & RefAttributes<El>,
 ): React.JSX.Element {
   if (typeof element === 'string') {
     return element
@@ -91,7 +98,6 @@ export function convert(
     switch (true) {
       case key === 'class': {
         attrs.className = val
-        delete elementAttributes.class
         break
       }
       case key === 'style': {
@@ -130,3 +136,5 @@ export function convert(
 
   return createElement(element.tag, { ...remaining, ...attrs }, ...children)
 }
+
+export const makeReactConverter = convert.bind(null, React.createElement)
diff --git a/src/index.ts b/src/index.ts
index 1ce1c48..39632f1 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,5 @@
 export * from './components/FontAwesomeIcon'
+export * from './components/FontAwesomeLayers'
 export * from './types/animation-props'
 export * from './types/icon-props'
 export * from './types/transform-props'
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 2b1b22e..a69dd18 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -18,6 +18,8 @@ export const SVG_CORE_VERSION =
 // Cache the version check result since it never changes during runtime
 export const IS_VERSION_7_OR_LATER = Number.parseInt(SVG_CORE_VERSION) >= 7
 
+export const DEFAULT_CLASSNAME_PREFIX = 'fa'
+
 export const ANIMATION_CLASSES = {
   beat: 'fa-beat',
   fade: 'fa-fade',
@@ -79,3 +81,7 @@ export const STYLE_CLASSES = {
   swapOpacity: 'fa-swap-opacity',
   widthAuto: 'fa-width-auto',
 } as const
+
+export const LAYER_CLASSES = {
+  default: 'fa-layers',
+} as const
diff --git a/src/utils/get-class-list-from-props.ts b/src/utils/get-class-list-from-props.ts
index b7b52a3..291cb69 100644
--- a/src/utils/get-class-list-from-props.ts
+++ b/src/utils/get-class-list-from-props.ts
@@ -7,12 +7,19 @@ import {
   SIZE_CLASSES,
   STYLE_CLASSES,
   IS_VERSION_7_OR_LATER,
+  DEFAULT_CLASSNAME_PREFIX,
 } from './constants'
 import { FontAwesomeIconProps } from '../types/icon-props'
 
-function withCustomPrefix(cls: string): string {
-  const customPrefix = config.cssPrefix || config.familyPrefix || 'fa'
-  return customPrefix === 'fa' ? cls : cls.replace('fa-', `${customPrefix}-`)
+export function withPrefix(cls: string): string {
+  const prefix =
+    config.cssPrefix || config.familyPrefix || DEFAULT_CLASSNAME_PREFIX
+  return prefix === DEFAULT_CLASSNAME_PREFIX
+    ? cls
+    : cls.replaceAll(
+        new RegExp(`(?<=^|\\s)${DEFAULT_CLASSNAME_PREFIX}-`, 'g'),
+        `${prefix}-`,
+      )
 }
 
 /**
@@ -87,11 +94,12 @@ export function getClassListFromProps(props: FontAwesomeIconProps): string[] {
   if (rotateBy) result.push(STYLE_CLASSES.rotateBy)
   if (widthAuto) result.push(STYLE_CLASSES.widthAuto)
 
-  const prefix = config.cssPrefix || config.familyPrefix || 'fa'
+  const prefix =
+    config.cssPrefix || config.familyPrefix || DEFAULT_CLASSNAME_PREFIX
 
-  return prefix === 'fa'
+  return prefix === DEFAULT_CLASSNAME_PREFIX
     ? result
     : // TODO: see if we can achieve custom prefix support without iterating
       // eslint-disable-next-line unicorn/no-array-callback-reference
-      result.map(withCustomPrefix)
+      result.map(withPrefix)
 }
diff --git a/tsup.config.ts b/tsup.config.ts
index b9fb344..378c30c 100644
--- a/tsup.config.ts
+++ b/tsup.config.ts
@@ -17,12 +17,12 @@ export default defineConfig([
     entry: ['src/index.ts'],
     outDir: 'dist',
   },
-  // Build config for React Server Components
+  // Build config for Client Components for SSR Apps (NextJS, Remix, etc.)
   {
     ...defaultConfig,
     name: 'react-fontawesome-rsc',
     entry: ['src/components/rsc/*.tsx'],
-    outDir: 'dist/components/rsc',
+    outDir: 'dist',
     bundle: false,
     treeshake: false,
   },



Test patch:
diff --git a/src/components/__tests__/FontAwesomeLayers.test.tsx b/src/components/__tests__/FontAwesomeLayers.test.tsx
new file mode 100644
index 0000000..eab7b5a
--- /dev/null
+++ b/src/components/__tests__/FontAwesomeLayers.test.tsx
@@ -0,0 +1,206 @@
+import React from 'react'
+
+import { text, counter, parse } from '@fortawesome/fontawesome-svg-core'
+import { render, screen } from '@testing-library/react'
+
+import { makeReactConverter } from '../../converter'
+import {
+  FontAwesomeLayers,
+  LayersText,
+  LayersCounter,
+} from '../FontAwesomeLayers'
+
+// Mock the converter module
+jest.mock('../../converter', () => ({
+  makeReactConverter: jest.fn((abstractElement, props) =>
+    React.createElement('span', {
+      ...props,
+      'data-testid': 'converted-element',
+    }),
+  ),
+}))
+
+// Mock fontawesome-svg-core
+// eslint-disable-next-line @typescript-eslint/no-unsafe-return
+jest.mock('@fortawesome/fontawesome-svg-core', () => ({
+  ...jest.requireActual('@fortawesome/fontawesome-svg-core'),
+  text: jest.fn(() => ({
+    abstract: [{ tag: 'span', attributes: {}, children: [] }],
+  })),
+  counter: jest.fn(() => ({
+    abstract: [{ tag: 'span', attributes: {}, children: [] }],
+  })),
+  parse: {
+    transform: jest.fn(),
+  },
+}))
+
+describe('FontAwesomeLayers', () => {
+  beforeEach(() => {
+    jest.clearAllMocks()
+  })
+
+  it('renders with default classes', () => {
+    render(<FontAwesomeLayers>content</FontAwesomeLayers>)
+
+    const element = screen.getByText('content')
+    expect(element).toHaveClass('fa-layers', 'fa-fw')
+  })
+
+  it('renders with custom className', () => {
+    render(
+      <FontAwesomeLayers className="custom-class">content</FontAwesomeLayers>,
+    )
+
+    const element = screen.getByText('content')
+    expect(element).toHaveClass('fa-layers', 'fa-fw', 'custom-class')
+  })
+
+  it('renders with size wrapper', () => {
+    render(<FontAwesomeLayers size="2x">content</FontAwesomeLayers>)
+
+    // eslint-disable-next-line testing-library/no-node-access
+    const wrapper = screen.getByText('content').parentElement
+    expect(wrapper).toHaveClass('fa-2x')
+  })
+
+  it('passes through HTML attributes', () => {
+    render(
+      <FontAwesomeLayers data-testid="layers" id="test-id">
+        content
+      </FontAwesomeLayers>,
+    )
+
+    const element = screen.getByTestId('layers')
+    expect(element).toHaveAttribute('id', 'test-id')
+  })
+
+  it('renders children correctly', () => {
+    render(
+      <FontAwesomeLayers>
+        <span>child1</span>
+        <span>child2</span>
+      </FontAwesomeLayers>,
+    )
+
+    expect(screen.getByText('child1')).toBeInTheDocument()
+    expect(screen.getByText('child2')).toBeInTheDocument()
+  })
+})
+
+describe('LayersText', () => {
+  it('renders text component', () => {
+    render(<LayersText text="Hello" />)
+
+    expect(screen.getByTestId('converted-element')).toBeInTheDocument()
+  })
+
+  it('handles inverse prop', () => {
+    render(<LayersText text="Hello" inverse />)
+
+    expect(text).toHaveBeenCalledWith(
+      'Hello',
+      expect.objectContaining({
+        classes: expect.arrayContaining(['fa-inverse']) as string[],
+      }),
+    )
+  })
+
+  it('handles className prop', () => {
+    render(<LayersText text="Hello" className="custom-class another-class" />)
+
+    expect(text).toHaveBeenCalledWith(
+      'Hello',
+      expect.objectContaining({
+        classes: expect.arrayContaining([
+          'custom-class',
+          'another-class',
+        ]) as string[],
+      }),
+    )
+  })
+
+  it('handles transform as string', () => {
+    render(<LayersText text="Hello" transform="shrink-6" />)
+
+    // eslint-disable-next-line @typescript-eslint/unbound-method
+    expect(parse.transform).toHaveBeenCalledWith('shrink-6')
+    expect(text).toHaveBeenCalled()
+  })
+
+  it('handles transform as object', () => {
+    const transformObj = { size: 0.5 }
+    render(<LayersText text="Hello" transform={transformObj} />)
+
+    expect(text).toHaveBeenCalledWith(
+      'Hello',
+      expect.objectContaining({
+        transform: transformObj,
+      }),
+    )
+  })
+
+  it('passes through style and attributes', () => {
+    const style = { color: 'red' }
+    render(<LayersText text="Hello" style={style} data-testid="text-layer" />)
+
+    expect(makeReactConverter).toHaveBeenCalledWith(
+      expect.any(Object),
+      expect.objectContaining({
+        style,
+        'data-testid': 'text-layer',
+      }),
+    )
+  })
+})
+
+describe('LayersCounter', () => {
+  it('renders counter component with number', () => {
+    render(<LayersCounter count={42} />)
+
+    expect(screen.getByTestId('converted-element')).toBeInTheDocument()
+  })
+
+  it('renders counter component with string', () => {
+    render(<LayersCounter count="99+" />)
+
+    expect(counter).toHaveBeenCalledWith('99+', expect.any(Object))
+  })
+
+  it('handles className prop', () => {
+    render(<LayersCounter count={5} className="badge red" />)
+
+    expect(counter).toHaveBeenCalledWith(
+      5,
+      expect.objectContaining({
+        classes: ['badge', 'red'],
+      }),
+    )
+  })
+
+  it('handles undefined className', () => {
+    render(<LayersCounter count={5} />)
+
+    expect(counter).toHaveBeenCalledWith(
+      5,
+      expect.objectContaining({
+        classes: undefined,
+      }),
+    )
+  })
+
+  it('passes through style and attributes', () => {
+    const style = { backgroundColor: 'tomato' }
+    render(
+      <LayersCounter count={10} style={style} data-testid="counter-layer" />,
+    )
+
+    expect(makeReactConverter).toHaveBeenCalledWith(
+      expect.any(Object),
+      expect.objectContaining({
+        style,
+        'data-testid': 'counter-layer',
+      }),
+    )
+  })
+})
diff --git a/src/utils/__tests__/get-class-list-from-props.test.ts b/src/utils/__tests__/get-class-list-from-props.test.ts
index 031791a..a817be5 100644
--- a/src/utils/__tests__/get-class-list-from-props.test.ts
+++ b/src/utils/__tests__/get-class-list-from-props.test.ts
@@ -7,7 +7,7 @@ import {
 
 import { FontAwesomeIconProps } from '../../types/icon-props'
 import { IS_VERSION_7_OR_LATER } from '../constants'
-import { getClassListFromProps } from '../get-class-list-from-props'
+import { getClassListFromProps, withPrefix } from '../get-class-list-from-props'
 
 describe('get class list', () => {
   const props = {
@@ -168,3 +168,29 @@ describe('get class list', () => {
     })
   })
 })
+
+describe('withPrefix utility function', () => {
+  const customPrefix = 'custom-prefix'
+
+  beforeEach(() => {
+    config.cssPrefix = customPrefix
+  })
+
+  it('should prefix a single class', () => {
+    expect(withPrefix('fa-icon')).toBe('custom-prefix-icon')
+  })
+
+  it('should prefix multiple space-separated classes', () => {
+    expect(withPrefix('fa-icon fa-spin fa-lg')).toBe(
+      'custom-prefix-icon custom-prefix-spin custom-prefix-lg',
+    )
+  })
+
+  it('should return empty string when given empty string', () => {
+    expect(withPrefix('')).toBe('')
+  })
+
+  it('should do nothing and return same string when given only whitespace', () => {
+    expect(withPrefix('   ')).toBe('   ')
+  })
+})