reactjswebpackweb-componentprimeicons

Issue while loading primeicons css resources into shadow dom


I would like to properly load css and icons into my web component, right now styles are loaded but icons are not. Here is my web component:

import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App.js";

import preactbase from "primereact/resources/primereact.css";
import preacttheme from "primereact/resources/themes/lara-light-blue/theme.css";
import picons from "primeicons/primeicons.css";

class Dumb extends HTMLElement {
  constructor() {
    super();
    console.log("HEY");
    this._shadowRoot = this.attachShadow({ mode: "open" });
    var style = document.createElement("style");
    style.textContent = preactbase + preacttheme + picons;
    var mountPoint = document.createElement("div");
    this._shadowRoot.appendChild(style);
    this._shadowRoot.appendChild(mountPoint);
    var reactroot = createRoot(mountPoint);
    reactroot.render(<App />);
  }

  connectedCallback() {}
}

window.customElements.define("dumb-app", Dumb);

As you can see, I am trying to wrap a minimal react spa which uses primeicons. App.js file is straightforward:

import "primeicons/primeicons.css";
import { Button } from "primereact/button";

function App() {
  return (
    <div>
      <h1>Hello from React!</h1>
      <i className="pi pi-check"></i>
      <br></br>
      <Button label="webcomponent" icon="pi pi-check" />
    </div>
  );
}

export default App;

I am using webpack with the following configuration:

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

const pkg = require("./package.json");
const commonPaths = require("./build-utils/config/commonPaths.js");

const isDebug = !process.argv.includes("release");
const port = process.env.PORT || 3000;

module.exports = {
  entry: commonPaths.entryPath,
  output: {
    uniqueName: pkg.name,
    publicPath: "/",
    path: commonPaths.outputPath,
    filename: `${pkg.version}/js/[name].[chunkhash:8].js`,
    chunkFilename: `${pkg.version}/js/[name].[chunkhash:8].js`,
    assetModuleFilename: isDebug ? `assets/[path][name].[contenthash:8][ext]` : `assets/[name].[contenthash:8][ext]`,
    crossOriginLoading: "anonymous",
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "public/index.html",
      filename: "index.html",
    }),
  ],
  devServer: {
    port,
    static: {
      directory: commonPaths.outputPath,
    },
    historyApiFallback: {
      index: "index.html",
    },
    webSocketServer: false,
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: ["babel-loader"],
      },
      {
        // Important: css-loader first (resolves url()), then to-string-loader
        test: /\.css$/i,
        use: [
          {
            loader: "to-string-loader",
          },
          {
            loader: "css-loader",
            options: { url: true },
          },
        ],
      },
      {
        // Inline modern webfonts into CSS (no network requests)
        test: /\.(woff|woff2)$/i,
        type: "asset/inline",
      },
      {
        // Other assets (images, etc.)
        test: /\.(png|jpg|jpeg|gif|svg|eot|ttf)$/i,
        type: "asset/resource",
        generator: {
          filename: "assets/[name].[hash][ext]",
        },
      },
    ],
  },
  resolve: {
    extensions: ["*", ".js", ".jsx"],
  },
};

And this is the package.json:

{
  "name": "y",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production",
    "start": "webpack serve --mode development"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@babel/preset-react": "^7.27.1",
    "primeicons": "^7.0.0",
    "primereact": "^10.9.7",
    "react": "^19.1.1",
    "react-dom": "^19.1.1",
    "to-string-loader": "^1.2.0"
  },
  "devDependencies": {
    "@babel/core": "^7.28.3",
    "@babel/preset-env": "^7.28.3",
    "babel-loader": "^10.0.0",
    "css-loader": "^7.1.2",
    "html-webpack-plugin": "^5.6.4",
    "raw-loader": "^4.0.2",
    "style-loader": "^4.0.0",
    "webpack": "^5.101.3",
    "webpack-cli": "^6.0.1",
    "webpack-dev-server": "^5.2.2"
  }
}

I can see what I am missing. I can see the <style> element filled with, I guess, all the styles I import, but no icon is redered. Full project can be fount here, thank you!


Solution

  • Here is what I did to get things done.

    I didn't load properly all the .css/.scss files. Style elements must use textContent to be filled with css/scss contents:

        import mycss from "./css/my.css";
        ...
        const shadow = this.attachShadow({ mode: "open" });
        ...
        const stylecss = document.createElement("style");
        stylecss.textContent = mycss;
        shadow.appendChild(stylecss);
    

    Configure webpack.config.js to load .css/.scss files properly. Here is a snippet:

        {
          test: /\.css$/,
          loader: "css-loader",
          options: {
            esModule: false,
            exportType: "string",
            url: true,
          },
          sideEffects: true,
        },
        {
          test: /\.(scss|sass)$/,
          use: [
            { loader: "to-string-loader" },
            { loader: "css-loader" },
            { loader: "sass-loader" },
          ],
          sideEffects: true,
        },
    

    Add primeicons into light DOM to let fonts be used in shadow DOM:

        import reacticons from "primeicons/primeicons.css";
        ...
        const globalStyle = document.createElement("style");
        globalStyle.textContent = reacticons;
        document.head.appendChild(globalStyle);
    

    Check if style is not already present in light to avoid attach it twice.

    Prevent dialogs or other element from being added to the light DOM: primereact attach dialogs to document.body by default so they won't receive styles loaded into shadow DOM. As primereact docs suggests, I did this to prevent that behavior:

        ...
        const configs = {
          appendTo: "self",
        };
    
        reactroot.render(
          <PrimeReactProvider value={configs}>
            <App messageboxes={messageboxes} />
          </PrimeReactProvider>,
        );