Rails 7 with React, TailwindCSS and Bootstrap 5 Example
– Rails 7.0.0
– SQLite
– Node v14.15.0
– NPM 6.14.8
– Yarn 1.22.17
– TailwindCSS 3
– Bootstrap 5
– React 17.0.2

Setup Rails 7 project

Create Rails 7 project

rails _7.0.0_ new Rails7WithReactTailwindCSSBootstrapExample -j esbuild -c tailwind

Install node packages

yarn add @tailwindcss/forms @tailwindcss/typography bootstrap @popperjs/core jquery postcss-flexbugs-fixes postcss-import postcss-nested postcss-preset-env react react-dom prop-types

Create PostCSS config file postcss.config.js

module.exports = {
  plugins: [
    require("autoprefixer"),
    require("postcss-import"),
    require("tailwindcss"),
    require("postcss-nested"),
    require("postcss-flexbugs-fixes"),
    require("postcss-preset-env")({
      autoprefixer: {
        flexbox: "no-2009",
      },
      stage: 3,
    }),
  ],
};

Create esbuild plugin to load css file esbuild.style.loader.plugin.js

// esbuild.style.loader.plugin.js

const fs = require('fs');

const styleLoaderPlugin = {
  name: 'styleLoader',
  setup: build => {
    // replace CSS imports with synthetic 'loadStyle' imports
    build.onLoad({ filter: /\.css$/ }, async args => {
      return {
        contents: `
          import {loadStyle} from 'loadStyle';
          loadStyle(${JSON.stringify(args.path)});
        `,
        loader: 'js',
      };
    });

    // resolve 'loadStyle' imports to the virtual loadStyleShim namespace which is this plugin
    build.onResolve({ filter: /^loadStyle$/ }, args => {
      return { path: `loadStyle(${JSON.stringify(args.importer)})`, namespace: 'loadStyleShim' };
    });

    // define the loadStyle() function that injects CSS as a style tag
    build.onLoad({ filter: /^loadStyle\(.*\)$/, namespace: 'loadStyleShim' }, async args => {
      const match = /^loadStyle\(\"(.*)"\)$/.exec(args.path);
      const cssFilePath = match[1];
      const cssFileContents = String(fs.readFileSync(cssFilePath));
      return {
        contents: `
          export function loadStyle() {
              const style = document.createElement('style');
              style.innerText = \`${cssFileContents}\`;
              document.querySelector('head').appendChild(style);
          }
        `,
      };
    });
  },
};

module.exports = {
  styleLoaderPlugin
};

Create file esbuild.config.js

// esbuild.config.js

const path = require('path')
const { styleLoaderPlugin } = require("./esbuild.style.loader.plugin");

require("esbuild").build({
  entryPoints: [
    'application.js',
    'react/hello_react.js',
    'styles/index.css'
  ],
  bundle: true,
  logLevel: 'info',
  outdir: path.join(process.cwd(), "app/assets/builds"),
  absWorkingDir: path.join(process.cwd(), "app/javascript"),
  watch: process.argv.includes("--watch"),
  publicPath: '/assets',
  loader: {
    '.js': 'jsx',
    '.png': 'file'
  },
  plugins: [
    styleLoaderPlugin
  ],
}).catch(() => process.exit(1))

Add build script to package.json

// ...
"scripts": {
    "build": "node esbuild.config.js",
    "build:css": "tailwindcss --postcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css"
},
// ...

Generate tailwindcss config tailwind.config.js

rm tailwind.config.js
npx tailwindcss init --full

Update Tailwind config tailwind.config.js

// ...
content: [
    './app/views/**/*.html.erb',
    './app/helpers/**/*.rb',
    './app/javascript/**/*.js'
],
prefix: 'tw-',
// ...

Create custom styles app/assets/stylesheets/styles.css

/* app/assets/stylesheets/styles.css */
/* Custom Styles */

.my-styles {
  font-weight: 600;
  color: green;
}

Update file application.tailwind.css

/* this line is used for the case if prioritizes Bootstrap first */
@import "bootstrap/dist/css/bootstrap.css";

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

/* this line is used for the case if prioritizes Bootstrap later */
/* @import "bootstrap/dist/css/bootstrap.css"; */
@import "./styles";

Copy images to app/assets/images/ base on this project
– app/assets/images/beams.jpeg
– app/assets/images/grid.svg
– app/assets/images/logo.svg

Generate home, hello_react pages

./bin/rails g controller pages home hello_react

Update route.rb

Rails.application.routes.draw do
  root 'pages#home'
  get '/hello_react' => 'pages#hello_react'
end

Setup Bootstrap and jQuery

mkdir app/javascript/libs
touch app/javascript/libs/bootstrap.js
touch app/javascript/libs/jquery.js
touch app/javascript/libs/index.js

Create bootstrap config app/javascript/libs/bootstrap.js

// app/javascript/libs/bootstrap.js

// import "bootstrap/dist/css/bootstrap.css"
// import "bootstrap/dist/js/bootstrap.bundle.js"

const bootstrap = require("bootstrap/dist/js/bootstrap.bundle.js")
const popoverElements = document.querySelector('[data-bs-toggle="popover"]')
if (popoverElements) new bootstrap.Popover(popoverElements, { trigger: 'hover' })

Create jQuery config app/javascript/libs/jquery.js

// app/javascript/libs/jquery.js
import jquery from 'jquery';
window.jQuery = jquery;
window.$ = jquery;

Create file index.js to include bootstrap and jquery app/javascript/libs/index.js

// app/javascript/libs/index.js
import "./jquery";
import "./bootstrap";

Update app/javascript/application.js

// Entry point for the build script in your package.json
import "@hotwired/turbo-rails"
import "./controllers"
import "./libs"

Create Tailwind components

app/views/pages/_content_home_tw.html.erb

<!-- app/views/pages/_content_home_tw.html.erb -->
<div class="tw-min-h-screen tw-bg-gray-50 tw-py-6 tw-flex tw-flex-col tw-justify-center tw-relative tw-overflow-hidden sm:tw-py-12">
  <img src="<%= image_url('beams.jpg') %>" alt="" class="tw-absolute tw-top-1/2 tw-left-1/2 tw--translate-x-1/2 tw--translate-y-1/2 tw-max-w-none" width="1308" />
  <div class="tw-absolute tw-inset-0 tw-bg-[url(<%= image_url('grid.svg') %>)] tw-bg-center [mask-image:tw-linear-gradient(180deg,white,rgba(255,255,255,0))]"></div>
  <div class="tw-relative tw-px-6 tw-pt-10 tw-pb-8 tw-bg-white tw-shadow-xl tw-ring-1 tw-ring-gray-900/5 sm:tw-max-w-lg sm:tw-mx-auto sm:tw-rounded-lg sm:tw-px-10">
    <div class="tw-max-w-md tw-mx-auto">
      <img src="<%= image_url('logo.svg') %>" class="tw-h-6" />
      <div class="tw-divide-y tw-divide-gray-300/50">
        <div class="tw-py-8 tw-text-base tw-leading-7 tw-space-y-6 tw-text-gray-600">
          <p>An advanced online playground for Tailwind CSS, including support for things like:</p>
          <ul class="tw-space-y-4">
            <li class="tw-flex tw-items-center">
              <svg class="tw-w-6 tw-h-6 tw-flex-none tw-fill-sky-100 tw-stroke-sky-500 tw-stroke-2" stroke-linecap="round" stroke-linejoin="round">
                <circle cx="12" cy="12" r="11" />
                <path d="m8 13 2.165 2.165a1 1 0 0 0 1.521-.126L16 9" fill="none" />
              </svg>
              <p class="tw-ml-4">
                Customizing your
                <code>tailwind.config.js</code> file
              </p>
            </li>
            <li class="tw-flex tw-items-center">
              <svg class="tw-w-6 tw-h-6 tw-flex-none tw-fill-sky-100 tw-stroke-sky-500 tw-stroke-2" stroke-linecap="round" stroke-linejoin="round">
                <circle cx="12" cy="12" r="11" />
                <path d="m8 13 2.165 2.165a1 1 0 0 0 1.521-.126L16 9" fill="none" />
              </svg>
              <p class="tw-ml-4">
                Extracting classes with
                <code>@apply</code>
              </p>
            </li>
            <li class="tw-flex tw-items-center">
              <svg class="tw-w-6 tw-h-6 tw-flex-none tw-fill-sky-100 tw-stroke-sky-500 tw-stroke-2" stroke-linecap="round" stroke-linejoin="round">
                <circle cx="12" cy="12" r="11" />
                <path d="m8 13 2.165 2.165a1 1 0 0 0 1.521-.126L16 9" fill="none" />
              </svg>
              <p class="tw-ml-4">Code completion with instant preview</p>
            </li>
          </ul>
          <p>Perfect for learning how the framework works, prototyping a new idea, or creating a demo to share online.</p>
        </div>
        <div class="tw-pt-8 tw-text-base tw-leading-7 tw-font-semibold">
          <p class="tw-text-gray-900">Want to dig deeper into Tailwind?</p>
          <p>
            <a href="https://tailwindcss.com/docs" class="tw-text-sky-500 hover:tw-text-sky-600">Read the docs →</a>
          </p>
        </div>
        <div class="tw-pt-8">
          <p class="my-styles">My Styles</p>
        </div>
      </div>
    </div>
  </div>
</div>

app/views/pages/_content_tailwind_1.html.erb

<div class="tw-mt-4 tw-mb-3">
  <div style="background-position:10px 10px" class="tw-not-prose tw-relative tw-bg-grid-gray-100 tw-bg-gray-50 tw-rounded-xl tw-overflow-hidden">
    <div class="tw-absolute tw-inset-0 tw-bg-gradient-to-b tw-from-gray-50 tw-opacity-60"></div>
    <div class="tw-relative tw-rounded-xl tw-overflow-auto tw-p-8">
      <div class="tw-flex tw-flex-col-- tw-sm:flex-row tw-justify-center tw-gap-8 tw-sm:gap-16">
        <div class="tw-flex tw-flex-col tw-items-center tw-tw-shrink-0">
          <p class="tw-font-medium tw-text-sm tw-text-gray-500 tw-font-mono tw-text-center tw-mb-3">shadow-cyan-500/50</p>
          <button class="tw-py-2 tw-px-3 tw-bg-cyan-500 tw-text-white tw-text-sm tw-font-semibold tw-rounded-md tw-shadow-lg tw-shadow-cyan-500/50 tw-focus:outline-none">Subscribe</button>
        </div>
        <div class="tw-flex tw-flex-col tw-items-center tw-tw-shrink-0">
          <p class="tw-font-medium tw-text-sm tw-text-gray-500 tw-font-mono tw-text-center tw-mb-3">shadow-blue-500/50</p>
          <button class="tw-py-2 tw-px-3 tw-bg-blue-500 tw-text-white tw-text-sm tw-font-semibold tw-rounded-md tw-shadow-lg tw-shadow-blue-500/50 tw-focus:outline-none">Subscribe</button>
        </div>
        <div class="tw-flex tw-flex-col tw-items-center tw-tw-shrink-0">
          <p class="tw-font-medium tw-text-sm tw-text-gray-500 tw-font-mono tw-text-center tw-mb-3">shadow-indigo-500/50</p>
          <button class="tw-py-2 tw-px-3 tw-bg-indigo-500 tw-text-white tw-text-sm tw-font-semibold tw-rounded-md tw-shadow-lg tw-shadow-indigo-500/50 tw-focus:outline-none">Subscribe</button>
        </div>
      </div>
    </div>
    <div class="tw-absolute inset-0 tw-pointer-events-none tw-border tw-border-black/5 tw-rounded-xl"></div>
  </div>
</div>

app/views/pages/_content_tailwind_2.html.erb

<div class="tw-relative tw-rounded-xl tw-overflow-auto">
  <!-- Snap Point -->
  <div class="tw-flex ml-[50%] tw-items-end tw-justify-start tw-pt-10 tw-mb-6">
    <div class="tw-ml-2 tw-rounded tw-font-mono text-[0.625rem] tw-leading-6 tw-px-1.5 tw-ring-1 tw-ring-inset tw-bg-indigo-50 tw-text-indigo-600 tw-ring-indigo-600">snap point</div>
    <div class="tw-absolute tw-top-0 tw-bottom-0 tw-left-1/2 tw-border-l tw-border-indigo-500"></div>
  </div>
  <!-- Contents -->
  <div class="tw-relative tw-w-full tw-flex tw-gap-6 tw-snap-x tw-overflow-x-auto tw-pb-14">
    <div class="tw-snap-center tw-shrink-0">
      <div class="tw-shrink-0 tw-w-4 sm:tw-w-48"></div>
    </div>
    <div class="tw-snap-center tw-shrink-0 first:tw-pl-8 last:tw-pr-8">
      <img class="tw-shrink-0 tw-w-80 tw-h-40 tw-rounded-lg tw-shadow-xl tw-bg-white" src="https://images.unsplash.com/photo-1604999565976-8913ad2ddb7c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&h=160&q=80">
    </div>
    <div class="tw-snap-center tw-shrink-0 first:tw-pl-8 last:tw-pr-8">
      <img class="tw-shrink-0 tw-w-80 tw-h-40 tw-rounded-lg tw-shadow-xl tw-bg-white" src="https://images.unsplash.com/photo-1540206351-d6465b3ac5c1?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&h=160&q=80">
    </div>
    <div class="tw-snap-center tw-shrink-0 first:tw-pl-8 last:tw-pr-8">
      <img class="tw-shrink-0 tw-w-80 tw-h-40 tw-rounded-lg tw-shadow-xl tw-bg-white" src="https://images.unsplash.com/photo-1622890806166-111d7f6c7c97?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&h=160&q=80">
    </div>
    <div class="tw-snap-center tw-shrink-0 first:tw-pl-8 last:tw-pr-8">
      <img class="tw-shrink-0 tw-w-80 tw-h-40 tw-rounded-lg tw-shadow-xl tw-bg-white" src="https://images.unsplash.com/photo-1590523277543-a94d2e4eb00b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&h=160&q=80">
    </div>
    <div class="tw-snap-center tw-shrink-0 first:tw-pl-8 last:tw-pr-8">
      <img class="tw-shrink-0 tw-w-80 tw-h-40 tw-rounded-lg tw-shadow-xl tw-bg-white" src="https://images.unsplash.com/photo-1575424909138-46b05e5919ec?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&h=160&q=80">
    </div>
    <div class="tw-snap-center tw-shrink-0 first:tw-pl-8 last:tw-pr-8">
      <img class="tw-shrink-0 tw-w-80 tw-h-40 tw-rounded-lg tw-shadow-xl tw-bg-white" src="https://images.unsplash.com/photo-1559333086-b0a56225a93c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&h=160&q=80">
    </div>
    <div class="tw-snap-center tw-shrink-0">
      <div class="tw-shrink-0 tw-w-4 sm:tw-w-48"></div>
    </div>
  </div>
</div>

app/views/pages/_content_tailwind_3.html.erb

<div class="tw-mt-4 tw--mb-3">
  <div class="not-prose tw-mb-4 tw-flex tw-space-x-2"><svg class="tw-flex-none tw-w-5 tw-h-5 tw-text-gray-400" viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="m9.813 9.25.346-5.138a1.276 1.276 0 0 0-2.54-.235L6.75 11.25 5.147 9.327a1.605 1.605 0 0 0-2.388-.085.018.018 0 0 0-.004.019l1.98 4.87a5 5 0 0 0 4.631 3.119h3.885a4 4 0 0 0 4-4v-1a3 3 0 0 0-3-3H9.813ZM3 5s.35-.47 1.25-.828m9.516-.422c2.078.593 3.484 1.5 3.484 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
    <p class="tw-text-gray-700 tw-text-sm tw-font-medium">Scroll in the tw-grid of images to see the expected behaviour</p>
  </div>
  <div class="not-prose tw-relative bg-grid-gray-100 tw-bg-gray-50 tw-rounded-xl tw-overflow-hidden" style="background-position: 10px 10px;">
    <div class="tw-absolute tw-inset-0 tw-bg-gradient-to-b tw-from-gray-50 tw-opacity-60"></div>
    <div class="tw-relative tw-rounded-xl tw-overflow-auto">
      <!-- Snap Point -->
      <div class="tw-flex ml-[50%] tw-items-end tw-justify-start tw-pt-10 tw-mb-6">
        <div class="tw-ml-2 tw-rounded tw-font-mono text-[0.625rem] tw-leading-6 tw-px-1.5 tw-ring-1 tw-ring-inset tw-bg-indigo-50 tw-text-indigo-600 tw-ring-indigo-600">snap point</div>
        <div class="tw-absolute tw-top-0 tw-bottom-0 tw-left-1/2 tw-border-l tw-border-indigo-500"></div>
      </div>
      <!-- Contents -->
      <div class="tw-relative tw-w-full tw-flex tw-gap-6 tw-snap-x tw-snap-mandatory tw-overflow-x-auto tw-pb-14">
        <div class="tw-snap-center tw-shrink-0">
          <div class="tw-shrink-0 tw-w-4 sm:tw-w-48"></div>
        </div>
        <div class="tw-snap-always tw-snap-center tw-shrink-0 first:tw-pl-8 last:tw-pr-8">
          <img class="tw-shrink-0 tw-w-80 tw-h-40 tw-object-cover tw-rounded-lg tw-shadow-xl tw-bg-white" src="https://images.unsplash.com/photo-1604999565976-8913ad2ddb7c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&h=160&q=80">
        </div>
        <div class="tw-snap-always tw-snap-center tw-shrink-0 first:tw-pl-8 last:tw-pr-8">
          <img class="tw-shrink-0 tw-w-80 tw-h-40 tw-object-cover tw-rounded-lg tw-shadow-xl tw-bg-white" src="https://images.unsplash.com/photo-1540206351-d6465b3ac5c1?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&h=160&q=80">
        </div>
        <div class="tw-snap-always tw-snap-center tw-shrink-0 first:tw-pl-8 last:tw-pr-8">
          <img class="tw-shrink-0 tw-w-80 tw-h-40 tw-object-cover tw-rounded-lg tw-shadow-xl tw-bg-white" src="https://images.unsplash.com/photo-1622890806166-111d7f6c7c97?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&h=160&q=80">
        </div>
        <div class="tw-snap-always tw-snap-center tw-shrink-0 first:tw-pl-8 last:tw-pr-8">
          <img class="tw-shrink-0 tw-w-80 tw-h-40 tw-object-cover tw-rounded-lg tw-shadow-xl tw-bg-white" src="https://images.unsplash.com/photo-1590523277543-a94d2e4eb00b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&h=160&q=80">
        </div>
        <div class="tw-snap-always tw-snap-center tw-shrink-0 first:tw-pl-8 last:tw-pr-8">
          <img class="tw-shrink-0 tw-w-80 tw-h-40 tw-object-cover tw-rounded-lg tw-shadow-xl tw-bg-white" src="https://images.unsplash.com/photo-1575424909138-46b05e5919ec?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&h=160&q=80">
        </div>
        <div class="tw-snap-always tw-snap-center tw-shrink-0 first:tw-pl-8 last:tw-pr-8">
          <img class="tw-shrink-0 tw-w-80 tw-h-40 tw-object-cover tw-rounded-lg tw-shadow-xl tw-bg-white" src="https://images.unsplash.com/photo-1559333086-b0a56225a93c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&h=160&q=80">
        </div>
        <div class="tw-snap-center tw-shrink-0">
          <div class="tw-shrink-0 tw-w-4 sm:tw-w-48"></div>
        </div>
      </div>
    </div>
    <div class="tw-absolute tw-inset-0 tw-pointer-events-none tw-border tw-border-black/5 tw-rounded-xl"></div>
  </div>
</div>

Create Bootstrap components

app/views/pages/_nav_bootstrap.html.erb

<div class="container-fluid">
  <nav class="navbar navbar-expand-lg navbar-light bg-light">
    <div class="container-fluid">
      <a class="navbar-brand" href="/" target="_top">BlogRails7</a>
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
          <li class="nav-item">
            <a class="nav-link active" aria-current="page" href="/" target="_top">Home</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="/hello_react" target="_top">Hello React</a>
          </li>
          <li class="nav-item dropdown">
            <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
              Dropdown
            </a>
            <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
              <li><a class="dropdown-item" href="#">Action</a></li>
              <li><a class="dropdown-item" href="#">Another action</a></li>
              <li><hr class="dropdown-divider"></li>
              <li><a class="dropdown-item" href="#">Something else here</a></li>
            </ul>
          </li>
          <li class="nav-item">
            <a class="nav-link disabled">Disabled</a>
          </li>
        </ul>
        <form class="d-flex">
          <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
          <button class="btn btn-outline-success" type="submit">Search</button>
        </form>
      </div>
    </div>
  </nav>
</div>

Create React component

app/javascript/react/components/MyClock/styles.css

.Clock {
  padding: 5px;
  margin-top: 15px;
  margin-left: auto;
  margin-right: auto;
}

app/javascript/react/components/MyClock/MyClock.js

import React, { Component } from 'react'
import PropTypes from 'prop-types';

export class MyClock extends Component {
  render() {
    return (
      <div>
        <div className="row">
          <div className="col-lg-12 tw-flex tw-justify-center">
            <Clock size={400} timeFormat="24hour" hourFormat="standard" />
          </div>
        </div>
      </div>
    );
  }
}

export default MyClock

export class Clock extends Component {
  constructor(props) {
    super(props);

    this.state = { time: new Date() };
    this.radius = this.props.size / 2;
    this.drawingContext = null;
    this.draw24hour = this.props.timeFormat.toLowerCase().trim() === "24hour";
    this.drawRoman = !this.draw24hour && this.props.hourFormat.toLowerCase().trim() === "roman";

  }

  componentDidMount() {
    this.getDrawingContext();
    this.timerId = setInterval(() => this.tick(), 1000);
  }

  componentWillUnmount() {
    clearInterval(this.timerId);
  }

  getDrawingContext() {
    this.drawingContext = this.refs.clockCanvas.getContext('2d');
    this.drawingContext.translate(this.radius, this.radius);
    this.radius *= 0.9;
  }

  tick() {
    this.setState({ time: new Date() });
    const radius = this.radius;
    let ctx = this.drawingContext;
    this.drawFace(ctx, radius);
    this.drawNumbers(ctx, radius);
    this.drawTicks(ctx, radius);
    this.drawTime(ctx, radius);
  }

  drawFace(ctx, radius) {
    ctx.beginPath();
    ctx.arc(0, 0, radius, 0, 2 * Math.PI);
    ctx.fillStyle = "white";
    ctx.fill();

    const grad = ctx.createRadialGradient(0, 0, radius * 0.95, 0, 0, radius * 1.05);
    grad.addColorStop(0, "#333");
    grad.addColorStop(0.5, "white");
    grad.addColorStop(1, "#333");
    ctx.strokeStyle = grad;
    ctx.lineWidth = radius * 0.1;
    ctx.stroke();

    ctx.beginPath();
    ctx.arc(0, 0, radius * 0.05, 0, 2 * Math.PI);
    ctx.fillStyle = "#333";
    ctx.fill();
  }

  drawNumbers(ctx, radius) {
    const romans = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"];
    const fontBig = radius * 0.15 + "px Arial";
    const fontSmall = radius * 0.075 + "px Arial";
    let ang, num;

    ctx.textBaseline = "middle";
    ctx.textAlign = "center";
    for (num = 1; num < 13; num++) {
      ang = num * Math.PI / 6;
      ctx.rotate(ang);
      ctx.translate(0, -radius * 0.78);
      ctx.rotate(-ang);
      ctx.font = fontBig;
      ctx.fillStyle = "black";
      ctx.fillText(this.drawRoman ? romans[num - 1] : num.toString(), 0, 0);
      ctx.rotate(ang);
      ctx.translate(0, radius * 0.78);
      ctx.rotate(-ang);

      // Draw inner numerals for 24 hour time format
      if (this.draw24hour) {
        ctx.rotate(ang);
        ctx.translate(0, -radius * 0.60);
        ctx.rotate(-ang);
        ctx.font = fontSmall;
        ctx.fillStyle = "red";
        ctx.fillText((num + 12).toString(), 0, 0);
        ctx.rotate(ang);
        ctx.translate(0, radius * 0.60);
        ctx.rotate(-ang);
      }
    }

    // Write author text
    ctx.font = fontSmall;
    ctx.fillStyle = "#3D3B3D";
    ctx.translate(0, radius * 0.30);
    ctx.fillText("React Clock", 0, 0);
    ctx.translate(0, -radius * 0.30);
  }

  drawTicks(ctx, radius) {
    let numTicks, tickAng, tickX, tickY;

    for (numTicks = 0; numTicks < 60; numTicks++) {

      tickAng = (numTicks * Math.PI / 30);
      tickX = radius * Math.sin(tickAng);
      tickY = -radius * Math.cos(tickAng);

      ctx.beginPath();
      ctx.lineWidth = radius * 0.010;
      ctx.moveTo(tickX, tickY);
      if (numTicks % 5 === 0) {
        ctx.lineTo(tickX * 0.88, tickY * 0.88);
      } else {
        ctx.lineTo(tickX * 0.92, tickY * 0.92);
      }
      ctx.stroke();
    }
  }

  drawTime(ctx, radius) {
    const now = this.state.time;
    let hour = now.getHours();
    let minute = now.getMinutes();
    let second = now.getSeconds();

    // hour
    hour %= 12;
    hour = (hour * Math.PI / 6) + (minute * Math.PI / (6 * 60)) + (second * Math.PI / (360 * 60));
    this.drawHand(ctx, hour, radius * 0.5, radius * 0.05);
    // minute
    minute = (minute * Math.PI / 30) + (second * Math.PI / (30 * 60));
    this.drawHand(ctx, minute, radius * 0.8, radius * 0.05);
    // second
    second = (second * Math.PI / 30);
    this.drawHand(ctx, second, radius * 0.9, radius * 0.02, "red");
  }

  drawHand(ctx, position, length, width, color) {
    color = color || "black";
    ctx.beginPath();
    ctx.lineWidth = width;
    ctx.lineCap = "round";
    ctx.fillStyle = color;
    ctx.strokeStyle = color;
    ctx.moveTo(0, 0);
    ctx.rotate(position);
    ctx.lineTo(0, -length);
    ctx.stroke();
    ctx.rotate(-position);
  }

  render() {
    return (
      <div className="Clock" style={{ width: String(this.props.size) + 'px' }}>
        <canvas width={this.props.size} height={this.props.size} ref="clockCanvas" />
      </div>
    );
  }
}

Clock.defaultProps = {
  size: 400, // size in pixels => size is length & width
  timeFormat: "24hour", // {standard | 24hour} => if '24hour', hourFormat must be 'standard'
  hourFormat: "standard" // {standard | roman}
};

Clock.propTypes = {
  size: PropTypes.number,
  timeFormat: PropTypes.string,
  hourFormat: PropTypes.string
};

app/javascript/react/components/App/styles.css

.my_styles_3 {
  font-family: 600;
  font-size: 2rem;
  color: pink;
  text-align: center;
}

app/javascript/react/components/App/index.js

import React from 'react'
import MyClock from '../MyClock/MyClock'
import "./styles.css";

export const App = () => {
  return (
    <div className="container tw-bg-gray-700 tw-rounded-xl tw-py-4">
      <div className={'tw-py-4'}>
        <div className={'my_styles_3'}>Hello React!!!</div>
      </div>
      <MyClock />
    </div>
  )
}

export default App

app/javascript/react/hello_react.js

import React from "react";
import { render } from "react-dom";
import App from "./components/App";

document.addEventListener("DOMContentLoaded", () => {
  render(<App />, document.getElementById('react-components'));
});

Render Tailwind and Bootstrap 5 components

Update file app/views/pages/home.html.erb

<%= render 'pages/nav_bootstrap' %>
<%= render 'pages/content_home_tw' %>

Render React components

Update file app/views/pages/hello_react.html.erb

<%= render 'pages/nav_bootstrap' %>
<%= render 'pages/popover_bs' %>

<div class="container mt-4 tw-rounded-xl tw-bg-gray-700">
  <h2 class="tw-text-3xl tw-text-white tw-py-4">React Components</h2>
</div>
<div id="react-components" class="tw-py-2"></div>
<%= javascript_include_tag "react/hello_react" %>

<div class="container tw-mt-8">
  <h2>Tailwind</h2>
  <%= render 'pages/content_tailwind_1' %>
  <%= render 'pages/content_tailwind_2' %>
  <%= render 'pages/content_tailwind_3' %>
  <div class="tw-py-8"></div>
</div>

app/javascript/styles/index.css

.my-styles-2 {
  font-weight: 600;
  color: red;
}

Create Article

./bin/rails g scaffold Article title:string body:text
./bin/rails db:create db:migrate

Use Tailwind and Bootstrap to update styles for article pages

Run app

./bin/dev

Then go to http://localhost:3000/

OR clone source and run to see the result:

cd
git clone https://github.com/ntamvl/Rails7WithReactTailwindCSSBootstrapExample.git
cd Rails7WithReactTailwindCSSBootstrapExample
bundle install
yarn install
./bin/dev

Screenshots:
Rails 7 with React, TailwindCSS and Bootstrap 5 Example
Rails 7 with React, TailwindCSS and Bootstrap 5 Example

Enjoy ^_^ :))

GitHub Repo: https://github.com/ntamvl/Rails7WithReactTailwindCSSBootstrapExample


References:
– https://rubyonrails.org/2021/12/15/Rails-7-fulfilling-a-vision
– https://getbootstrap.com/docs/5.1/getting-started/introduction/
– https://tailwindcss.com/docs/installation