Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to inject CSS files with using shadow dom for content scripts. #609

Open
1 of 2 tasks
demirelio opened this issue Dec 17, 2022 · 18 comments
Open
1 of 2 tasks

How to inject CSS files with using shadow dom for content scripts. #609

demirelio opened this issue Dec 17, 2022 · 18 comments
Labels
vue Vue related issue

Comments

@demirelio
Copy link

demirelio commented Dec 17, 2022

Build tool

Vite

Where do you see the problem?

  • In the browser
  • In the terminal

Describe the bug

I created a shadow dom for my content scripts to prevent css leak from host website. I have my react component that import css file but it doesn't work inside shadow dom. When I control the network tab, I see that css file is actually accessible in host website but it doesn't work inside shadow dom. The css file is imported to component sits inside shadow dom.

CONTENT-ENTRY

//This file is the entry point for the content script
//We work directly on the website DOM and we can use any of the browser APIs
//There is shadow dom here.
import React from "react";
import { createRoot } from "react-dom/client";
import ContentRoot from "./routes/content-root";
import ErrorPage from "./error-page";
import { RouterProvider, createMemoryRouter } from "react-router-dom";

//This code creates a root div and appends it into the website
const contentRoot = document.createElement("div");
contentRoot.id = "nc-root";
const shadowRoot = contentRoot.attachShadow({ mode: "open" });
const shadowWrapper = document.createElement("div");
shadowWrapper.id = "root";
document.body.append(contentRoot);
shadowRoot.append(shadowWrapper);

//It creates our main memory router
const router = createMemoryRouter([
  {
    path: "/",
    element: <ContentRoot />,
    errorElement: <ErrorPage />,
  },
  {
    path: "/todos",
    element: <div>todos</div>,
  },
]);

const root = createRoot(shadowWrapper);
root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

APP

// This is the root component for the content script
// Check content-entry for direct DOM manipulation
import { useState } from "react";
import { Link } from "react-router-dom";
import "../styles/content-root.css";

function ContentRoot() {
  const [count, setCount] = useState(0);

  return (
    <div className="app">
      <header>
        <p className="big-title">Hello Vite + React! He</p>
        <p>
          <button type="button" onClick={() => setCount((count) => count + 1)}>
            count is: {count}
          </button>
        </p>
        <p>
          <Link to="todos">Click for todos</Link>
        </p>
        <a href="#" className="test">
          TEST
        </a>
      </header>
    </div>
  );
}

export default ContentRoot;

CSS
.big-title { color: purple; } .test { color: green; }

Reproduction

  1. Create a shadow dom
  2. Import your react app inside that shadow dom
  3. Create a CSS file and import it into your component
  4. Your component is rendered without CSS.
  5. CSS file is accessible in your browser network tab.

Logs

No response

System Info

N/A

Severity

annoyance

@willjohnathan
Copy link

willjohnathan commented Dec 18, 2022

this is similar to what I have in my content-script.js. You should be able to adjust it to work at the component level too. Still has some HMR issues though.

import css from "../styles/content-root.css?inline";

// create variable to attach the tailwind stylesheet
let style = document.createElement('style');

// attach the stylesheet as text
style.textContent = css;

// apply the style
shadowWrapper.appendChild(style);

@sghsri
Copy link

sghsri commented Dec 19, 2022

@willjohnathan would that only add the root css? what about the css for like individual components? I usually import those within the component files themselves

@willjohnathan
Copy link

willjohnathan commented Dec 19, 2022

I'm actually not too familiar with react, but couldn't you just manually add a style tag. I suppose it would leak from compoenent to compenent though.

// This is the root component for the content script
// Check content-entry for direct DOM manipulation
import { useState } from "react";
import { Link } from "react-router-dom";
import css from "../styles/content-root.css";

function ContentRoot() {
  const [count, setCount] = useState(0);

  return (
    <div className="app">
      <header>
        <p className="big-title">Hello Vite + React! He</p>
        <p>
          <button type="button" onClick={() => setCount((count) => count + 1)}>
            count is: {count}
          </button>
        </p>
        <p>
          <Link to="todos">Click for todos</Link>
        </p>
        <a href="#" className="test">
          TEST
        </a>
      </header>
      <style>{css}</style>
    </div>
  );
}

export default ContentRoot;

@demirelio
Copy link
Author

I'm actually not too familiar with react, but couldn't you just manually add a style tag. I suppose it would leak from compoenent to compenent though.

// This is the root component for the content script
// Check content-entry for direct DOM manipulation
import { useState } from "react";
import { Link } from "react-router-dom";
import css from "../styles/content-root.css";

function ContentRoot() {
  const [count, setCount] = useState(0);

  return (
    <div className="app">
      <header>
        <p className="big-title">Hello Vite + React! He</p>
        <p>
          <button type="button" onClick={() => setCount((count) => count + 1)}>
            count is: {count}
          </button>
        </p>
        <p>
          <Link to="todos">Click for todos</Link>
        </p>
        <a href="#" className="test">
          TEST
        </a>
      </header>
      <style>{css}</style>
    </div>
  );
}

export default ContentRoot;

This is not working because, as far as I understand CRXJS Vite setup injects CSS import to the page header. If you work inside shadow dom, those styles are not accessible. That was my point, how can we make the vite add those styles in the component body when we use shadow dom?

@demirelio
Copy link
Author

this is similar to what I have in my content-script.js. You should be able to adjust it to work at the component level too. Still has some HMR issues though.

import css from "../styles/content-root.css?inline";

// create variable to attach the tailwind stylesheet
let style = document.createElement('style');

// attach the stylesheet as text
style.textContent = css;

// apply the style
shadowWrapper.appendChild(style);

I tried this and couldn't make it work tbh. What I end up using is removing the shadow dom and using tailwind with config that adds auto important to all styles.

I would love to have a solution where we can use Tailwind with shadow dom. I think it's a matter of a new vite config, but I have no idea how to do it. I don't know how to handle bundlers :D

@willjohnathan
Copy link

willjohnathan commented Dec 19, 2022

I use tailwind in the shadow dom similar to as mentioned above, but with Vue. I tried to do it with Svelte, but ran into a similar issue you are describing with vite still applying to the head as well. I couldn't do the prefix or important method, because it still would reset all the base html element styles (i.e. input, h2, etc.).

Also, you might find this helpful, he modifies the vite file to look in the shadow-dom to update the css instead of the head (but I believe it was for HMR):
#239 (comment)

@demirelio
Copy link
Author

I use tailwind in the shadow dom similar to as mentioned above, but with Vue. I tried to do it with Svelte, but ran into a similar issue you are describing with vite still applying to the head as well. I couldn't do the prefix or important method, because it still would reset all the base html element styles (i.e. input, h2, etc.).

Also, you might find this helpful, he modifies the vite file to look in the shadow-dom to update the css instead of the head (but I believe it was for HMR): #239 (comment)

I will check this out, thanks. I also disabled to base styles for tailwind. I simply use prefix, important and disabled base styles atm. I want to use shadow-dom but my lack of vite knowledge is preventing me from doing it atm :)

@A-Shleifman
Copy link
Contributor

It's not a bug. Vite sees .css imports and automatically injects these styles into the main DOM.

If you add ?inline to the import statement, Vite will give you a string containing the styles without injecting them. You can then add them inside the Shadow DOM.

import styles from 'index.css?inline';

render(
  <StrictMode>
    <style type="text/css">{styles}</style>

    <div><Component /></div>
  </StrictMode>
);

@demirelio
Copy link
Author

It's not a bug. Vite sees .css imports and automatically injects these styles into the main DOM.

If you add ?inline to the import statement, Vite will give you a string containing the styles without injecting them. You can then add them inside the Shadow DOM.

import styles from 'index.css?inline';

render(
  <StrictMode>
    <style type="text/css">{styles}</style>

    <div><Component /></div>
  </StrictMode>
);

This works, but how do you handle HMR? With this approach, dev server doesn't see tailwind classes until you restart the dev server.

@A-Shleifman
Copy link
Contributor

Most of the time re-saving the index.css file is enough. Of course, it's not a solution. I opened a separate issue #600 regarding HMR a while ago. This one can probably be closed.

@IamFive
Copy link

IamFive commented Apr 19, 2023

?inline

in this case, how to handle 3rd lib css?

@A-Shleifman
Copy link
Contributor

It depends on the lib. If the lib comes with its own CSS that you need to import, you can do the same (add ?inline to the import), otherwise... you could render your code in an iframe which is less performant and will make communication with the main frame much harder, but it would completely isolate the styles.

That's one of the reasons why headless libraries are getting more popular. They provide all the functionality, but 0 CSS and it is up to you and your design requirements how to style it.

Examples: TanStack Table, Headless UI, Downshift

@thmsmlr
Copy link

thmsmlr commented May 25, 2023

Hey y'all, I found a work around that unblocks me in development.

What I found is that if you import "index.css" and modify the index.css file HMR does work properly. This has two obvious problems though:

  1. import "index.css" auto injects the css into the document.head which is undesirable because we want it to only apply to the shadow root.
  2. If you're using tailwindcss you aren't directly modifying the index.css file, so the HMR won't actually trigger.

However, it is enough to get a little hack going. In my app i'm using svelte in the content script that is mounted in a closed shadow root. Ignoring the mounting code, I have a svelte component that looks something like

<script lang="ts">
  import css from "./../app.css?inline";
  let styleTag = `<${""}style>${css}</style>`;

  // WARNING: This is a hack
  import "./../app.css";
  let tag = document.querySelector('[data-vite-dev-id]')
  if(tag instanceof HTMLStyleElement) {
      tag.media = 'max-width: 0px';
      styleTag = `<${""}style>${tag.innerText}</style>`;
  }
</script>

{@html styleTag}

<h1 class="text-3xl bg-green-400">hello world</h1>

Okay so to solve the first issue, I find the style tag that vite injects into the head, and I nullify it's effects on the page by setting styletag.media = 'max-width: 0px' this scopes the innerText of the tag to only apply when the browser has zero width, so basically always disabled. Then I create a style tag with the same contents as the one vite injected into the document.head, then svelte puts it in the shadow root via the {@html styleTag}. This can be pretty trivially adopted for react or w/e framework.

Then for the second issue, I create a VIM autocmd autocmd BufWritePost * silent! !touch src/app.css which does a zero edit change to the css via touch any time any file in my project is written. If you aren't a VIM user, i'm sure there is something equivalent in VSCode.

It's obviously not ideal, but it gets HMR working with shadow root CSS with Tailwind in development which is good enough for me for now.

@gary-lo
Copy link

gary-lo commented Jun 15, 2023

I use tailwind in the shadow dom similar to as mentioned above, but with Vue. I tried to do it with Svelte, but ran into a similar issue you are describing with vite still applying to the head as well. I couldn't do the prefix or important method, because it still would reset all the base html element styles (i.e. input, h2, etc.).
Also, you might find this helpful, he modifies the vite file to look in the shadow-dom to update the css instead of the head (but I believe it was for HMR): #239 (comment)

I will check this out, thanks. I also disabled to base styles for tailwind. I simply use prefix, important and disabled base styles atm. I want to use shadow-dom but my lack of vite knowledge is preventing me from doing it atm :)

Any chance you could show me the snippet of config to do the above @demirelio - I tried adding prefix and important but it still affects the base site. I'm probably missing this disabled base styles what is this?

@ziontee113
Copy link

ziontee113 commented Jun 28, 2023

Thanks @thmsmlr for the suggestion. I'm currently using this in my nvim config:

local augroup = vim.api.nvim_create_augroup("Tailwind HMR for crxjs", { clear = true })
local filetypes_to_check = { "typescriptreact", "javascriptreact", "svelte", "vue" }
vim.api.nvim_create_autocmd({ "BufWritePost" }, {
    pattern = "*",
    group = augroup,
    callback = function()
        if vim.tbl_contains(filetypes_to_check, vim.bo.ft) then
            local result = vim.fn.filereadable("manifest.json")
            if result == 1 then
                vim.cmd("silent! !touch src/index.css")
            end
        end
    end,
})

I don't really need to configure anything crazy in my project at all. Just auto reload the index.css file and HMR seems to work as expected.

Edit: I ran into the problem of leaking Tailwind CSS into the main DOM. I'm using this in my postcss.config.js to contain the leak.
I had to npm i -D postcss-nested.

export default {
  plugins: {
    "postcss-nested": {},
    tailwindcss: {},
    autoprefixer: {},
  },
};

My index.css

#crx-root {
  @tailwind base;
  @tailwind components;
  @tailwind utilities;
}

Tailwind compiler will complain, but everything works fine for me atm.

@sovetski
Copy link

Thank you, @ziontee113, you saved my day!!!!

@ch0c0l8ra1n
Copy link

Imo crxjs should have a feature specifically for injecting a react component into a shadow dom. I spent the entire day today figuring out how to handle css in shadowdom with crxjs.

Right now what I’m doing is running building once, placing the bundled css into my public folder and then building it again. If anyone here had a better solution it would be very helpful.

If there was a way to just include the bundled css in the manifest file directly under web accessible resources, that would not solve everything but it would certainly help.

@ch0c0l8ra1n
Copy link

Another solution I have figured out right now is by using "vite-plugin-css-injected-by-js". I have the plugin inject the bundled css as a style tag with an id, whose css I then add to another style tag I create in the shadow dom. This fixes most of the issues with working with shadow dom and crxjs but sadly it's not compatible with the vite dev server some reason, so no hmr. However, I still think the tradeoff is worth it. I am now running vite build --watch and web-ext run to emulate a speedy dev environment. It's not quite hmr but is good enough considering I absolutely needed the css to work in a shadow dom.

@IamFive this method also works with 3rd party libs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
vue Vue related issue
Projects
None yet
Development

No branches or pull requests