cssruby-on-railswebpacktailwind-csscss-variables

How to change tailwind-config.js dynamically based on user settings in rails


I have a Rails 6 app set up to use Tailwind CSS with Webpacker similarly to how it's done in this GoRails tutorial.

I want to be able to change the Tailwind defaults dynamically based on the controller and action so that it's very easy for users to "skin" sections of the site by selecting a few options that then dynamically adjust a few of the Tailwind config options. (An example of how this could be used would be users logged into the admin area of the site changing their font family and background color to match their brand.)

I can't just add a stylesheet to the layout based on a conditional because I'd have to override all of the instances where a Tailwind css variable I want to change (like "sans-serif"). That would be a lot of work and brittle to maintain as Tailwind evolves.

It would be ideal if there was a way to dynamically insert choices selected by the user into the Tailwind config file (/javascript/stylesheets/tailwindcss-config.js), but I'm not sure how to do this.

Also is there a better way to do this in Rails when using Tailwind? It seems like there should be some way to use Javascript from the controller to dynamically change the settings in my tailwindcss-config.js (The Tailwind config file is explained here). So, something in that file like this:

theme: {
    fontFamily: {
      display: ['Gilroy', 'sans-serif'],
      body: ['Graphik', 'sans-serif'],
    },

What was a font stack hard-coded as a configuration in Tailwind would become this:

theme: {
    fontFamily: {
      display: DYNAMICALLY INSERTED FONT STACK,
      body: ANOTHER DYNAMICALLY INSERTED FONT STACK,
    },

How would you do this in Rails? I have that Tailwind config file living at /javascript/stylesheets/tailwindcss-config.js. Is this possible to do with Webpack in rails? Is this even the correct approach to take with Rails 6 using Webpacker + Tailwind?


Solution

  • I have the feeling that we'd be trying to use a 'buildtime' tool for a 'runtime' operation

    To directly inject the variable into tailwindcss config file would imply a rebuild of the actual css served to the user, applying the instructions in tailwind config file to the actual content put in app/javascript/css (assuming the setup used in the mentioned video tutorial).

    The operation is carried on by webpack, integrated through the webpacker gem.

    IMHO, neither webpack nor tailwind were designed with the purpose of rebuilding the assets at runtime, and, even if I'm definitely aware that a universal machine can do anything ;) I wonder where taking this route would take one, mainly in terms of maintainability.

    From this link it seems that triggering a rebuild of webpack on a config change is not straightforward.

    Here's a somewhat different path to try:

    In the <head> section of the application define css variables (more precisely 'css custom properties') for the settings you want your user to access, which can be set and changed dynamically (from js too)

    <style>
      :root{
        --display-font: "<%= display_font_families %>";
        --body-font: "<%= body_font_families %>";
        --link-color: "<%= link_color %>";
      }
    </style>
    

    Alternatively you could create app/assets/stylesheets/root.css.erb (the extension is important) and include it in your template before tailwind

    Then you should be able to change your tailwindcss config to something like the following:

    theme: {
        fontFamily: {
          display: "var(--display-font)",
          body: "var(--body-font)",
        },
        extend: {
          colors: {
            link: "var(--link-color)",
          },
        }
    

    This way we define a dynamic css layout that responds to the value of css variables. The variables and the structure they act on reside on the same logical level, which corresponds to the actual webpage served to the user.

    css variables are easily accessible from js, this is one way to have a clean access from rails too


    Now let's imagine that the user wants to change the link color (applied to all the links).

    In our imaginary settings form, she chooses an arbitrary color (in any css-valid format - the only constraint here is that it must be a valid css value, something you'll need to address with some form of input validation).

    We'd likely want

    // userSelectedColor is the result of a user's choice, 
    // say it's "#00FF00"
    
    document.documentElement.style
        .setProperty('--link-color', userSelectedColor);
    

    as soon as this value is changed, all the classes previously created by tailwind, and any rule that make use of the variable, will reflect the change, no need to rebuild the css at all.

    Please note that our user is not constrained to an arbitrary subset of the possible values, anything that can be accepted by css is fair game. By assigning to the config parameter a css variable, we actually have instructed tailwindcss to specify it in all its classes as a variable value, which now is under our control through css/js ... We definitely DON'T NEED (nor want) webpack to rebuild the styles

    To try to make it clearer, with our color example, in the generated css there will be classes like these - have a look at this link for an explanation of how customizing tailwind theme works

    /* GENERATED BY TAILWIND - well, this or something very similar :) */
    
    .text-link {
        color: var(--link-color);
    }
    .bg-link{
        background-color: var(--link-color);
    }
    /* .border-link { ... */
    

    clearly the browser needs to know the value of --link-color (we've defined it in the :root section) and the value itself can be any valid css, but what interests us is that it can be changed anytime, automagically propagating the change to every rule using it, it's a css variable ...

    this is plainly accomplished (for example) handling the form submit, saving the new value, which will then be pulled from the db to valorize the css variables on the next render of the page

    just my 2 cents :) have fun !