Skip to content
About Garden

Blog

[CSS] Switching theme in Tailwind

Basic Knowledge: CSS, HTML, JS, React

Tailwind provide dark mode to set dark theme style individually by adding keyword dark: in front of any CSS class:

<div class="text-black dark:text-white">
{/* ... */}
</div>

If we want to make a theme switcher, it seems feasible by using dark:. But it will be restricted to only TWO themes. Another disadvantage is, since we use dark: syntax in CSS class on HTML elements, we have to add all of it no matter where the theme is.

For who want to skip all only to see the final result & source code, please go here.

As Tailwind provides custom color in its config file:

module.exports = {
theme: {
colors: {
primary: "#0d6efd",
secondary: "#6c757d",
danger: "#dc3545",
warning: "#ffc107",
},
},
};

PS. The color above is referred to Bootstrap color palette.

Suppose we had two themes, config might be looked like this:

module.exports = {
theme: {
colors: {
"theme-1-primary": "#0d6efd",
"theme-1-secondary": "#6c757d",
"theme-1-danger": "#dc3545",
"theme-1-warning": "#ffc107",
"theme-2-primary": "#0d6efd",
"theme-2-secondary": "#6c757d",
"theme-2-danger": "#dc3545",
"theme-2-warning": "#ffc107",
},
},
};

Therefore, we can change class by what current theme is:

function Button() {
const buttonTheme = isTheme1 ? 'bg-theme-1-primary' : 'bg-theme-2-primary';
return <button className={buttonTheme}>;
}

But thinking of write the logic EVERYWHERE relate to theme, it’s not a smart way. In addition, if we had four or five themes, it could not be written with one-liner. We might have bunch of switch cases.

With CCS variables (or custom properties), we can define variables in CSS. The CSS variables can be divided into two parts: global & scope variables.

Here’s how to define CSS scope variavble:

element {
--main-bg-color: brown;
}

The variable --main-bg-color can only be used in THIS scope. (namely, THIS curly brackets {})

The more common way is to define variables in pseudo-class :root, all variables in here can be used under HTML document, namely global variables.

:root {
--main-bg-color: brown;
}

When using CSS variables, calling var() function with variable name as argument:

element {
background-color: var(--main-bg-color);
}

Combining Tailwind and CSS variables, we can achieve theme switching. We just modified the config a little bit by replacing color code with CSS variables, like this:

module.exports = {
theme: {
colors: {
'color-one': 'var(--color-one)',
'color-two': 'var(--color-two)',
'color-three': 'var(--color-three)',
'color-four': 'var(--color-four)',
'color-five': 'var(--color-five)',
},
}
}

As we’ve done with Tailwind config, another part is defining each theme palette, along with their global variables.

const theme_ayanami = `
:root {
--color-one: #1d446c;
--color-two: #f1f1f1;
--color-three: #571a1a;
--color-four: #000000;
--color-five: #525252;
}
`;
const theme_ikari = `
:root {
--color-one: #3f6d4e;
--color-two: #8bd450;
--color-three: #1d1a2f;
--color-four: #965fd4;
--color-five: #734f9a;
}
`;

After wrapping theme variables in <style>, we implant it into <head>.

function App() {
const [currentTheme, setCurrentTheme] = useState("ayanami");
const getThemeVariables = (_theme) => {
switch (_theme) {
case "ayanami":
return theme_ayanami;
break;
case "ikari":
return theme_ikari;
break;
}
};
useEffect(() => {
if (!document.getElementById("customThemeId")) {
const head = document.head;
const newStyleElement = document.createElement("style");
head.appendChild(newStyleElement);
newStyleElement.id = "customThemeId";
newStyleElement.innerHTML = getThemeVariables(currentTheme);
} else {
const styleElement = document.getElementById("customThemeId");
if (styleElement) {
// Update CSS gloabal variables
styleElement.innerHTML = getThemeVariables(currentTheme);
}
}
}, [currentTheme]);
return (
<div>
<button onClick={() => setCurrentTheme("ayanami")}>Ayanami</button>
<button onClick={() => setCurrentTheme("ikari")}>Ikari</button>
</div>
);
}

Here we define two themes, Eva.00 and Eva.01. After that, we can use the custom class we defined:

<div class="bg-color-one"></div>

Through modifying the content in <style>, we can overwrite old theme color palette with new one. Then we can change theme!

As checking dev tools of browser, we can find that only global variables change, everything inside the <body> will remain unchanged.

Encore! Here are another two theme! Hatsune Miku and Suzumiya Haruhi themes! 😘

For seeing final result, please go to here.

For all source code, please refer to here.

As we change theme, we don’t have to modify any line of code in HTML, CSS class. Also theme change logic is now unnecessary in any component.

Every color theme variables set is just a string, ready to implant into <style>. In the exmample above, I just simply place them in JS files. When it comes to updating theme or adding new theme, we still have to build all front-end project every time. To prevent this, we have to consider it further in CI/CD structure and strategy. Since I’m not familiar with CI/CD, so I don’t discuss it here. I would be glad to hear if anyone who had a better solution to integrate it with CI/CD pipeline.

Have a nice day and happy coding. 😎

The First Dive in Multi-Threaded Patterns

Here’s a little side note from Chapter 6 - Multithreaded Patterns in this book: Multithreaded JavaScript.

In this chapter, introducing some multi-threaded patterns:

  1. Thread Pool
  2. Mutex
  3. Ring Buffers
  4. Actor Model
  • The thread pool is a very popular pattern that is used in most multithreaded applications in some form or another.
  • A thread pool is a collection of homogeneous worker threads that are each capable of carrying out CPU-intensive tasks that the application may depend on.
  • libuv library that Node.js depends on provides a thread pool, defaulting to four threads, for performing low-level I/O operations.
  • This pattern might feel similar to distributed systems.
  • Discuss thread into two parts: pool size and dispatch strategies.
  • Typically, the size of a thread pool won’t need to dynamically change throughout the lifetime of an application.
  • With most operating systems there is not a direct correlation between a thread and a CPU core.
  • Having too many threads compared to the number of CPU cores can cause a loss of performance.
  • The constant context switching will actually make an application slower.
  • Thread pool contains: worker thread, main thread, garbage collection thread (if using libuv)
Node.js
// browser
cores = navigator.hardwareConcurrency;
cores = require("os").cpus().length;
  • Don’t forget the main thread, so total threads are n + 1
  • Deciding how many threads by purpose:
    • Cryptocurrency miner that does 99.9% of the work in each thread and almost no I/O and no work in the main thread. Using the number of available cores as the size of the thread pool might be OK.
    • Video streaming and transcoding service that performs heavy CPU and heavy I/O. You may want to use the number of available cores minus two.
  • Reasonable starting point might be to use the number of available cores minus one and then tweak when necessary.
  • A naive approach might be to just collect tasks to be done, then pass them in once the number of tasks ready to be performed meets the number of worker threads and continue once they all complete.
  • However, each task isn’t guaranteed to take the same amount of time to complete.

Here’s a list of the most common strategies:

  • Each task is given to the next worker in the pool, wrapping around to the beginning once the end has been hit.
  • The benefit of this is that each thread gets the exact same number of tasks to perform.
  • Unfair distribution of work.
  • The HAProxy reverse proxy refers to this as roundrobin.
  • Each task is assigned to a random worker in the pool.
  • Possibly unfair distribution of work.
  • When a new task comes along it is given to the least busy worker.
  • When two workers have a tie for the least amount of work, then one can be chosen randomly.
  • HAProxy refers to this as leastconn.
  • Mutex means mutually exclusive lock.
  • A mechanism for controlling access to some shared data.
  • It ensures that only one task may use that resource at any given time.
  • A task acquires the lock in order to run code that accesses the shared data, and then releases the lock once it’s done.
  • The code between the acquisition and the release is called the critical section.
  • A ring buffer is an implementation of a first-in-first-out (FIFO) queue, implemented using a pair of indices into an array of data in memory.
  • The array is treated as if one end is connected to the other, creating a ring of data. This means that if these indices are incremented past the end of the array, they’ll go back to the beginning.
  • An analog in the physical world is the restaurant order wheel, commonly found in North American diners.
  • head index: The head index refers to the next position to add data into the queue.
  • tail index: The tail index refers to the next position to read data out of the queue from.
  • buffer capacity (length): The capacity of the buffer.

Refer to the chart from the book:

ring buffer

  • When the data is written into buffer, head index will move to the next position.
  • When the data is read from the buffer, tail index will move to the next position.
  • When head or tail index at the last position of buffer, next will move the the first position of buffer.
  • Since it’s a RING buffer, there’s no start and end point. Ths start position of head and tail index does not matter.
  • tail index is always located behind or at the same position with head index.
  • When the buffer is FULL, there’s two strategies for this situation:
    • Overwrite the oldest: Overwrite the oldest data in the buffer. It means that newer data is more important.
    • Prevent from writing: Throw an error, banning the new data from writing into the buffer.
  • It is ALWAYS necessary to get the oldest data in the buffer correctly.

Refer to wikis:

  • The useful property of a circular buffer is that it does not need to have its elements shuffled around when one is consumed.
  • The circular buffer is well-suited as a FIFO (first in, first out) buffer.
  • The non-circular buffer is well suited as a LIFO (last in, first out) buffer.
  • The idea of stake in JavaScript meets the concept of LIFO.
  • Circular buffering makes a good implementation strategy for a queue that has fixed maximum size.
  • For arbitrarily expanding queues, a linked list approach may be preferred instead.
  • The actor model is a programming pattern for performing concurrent computation.
  • An actor is a primitive container that allows for executing code.
  • An actor is a first-class citizen in the Erlang programming language, but it can certainly be emulated using JavaScript.
  • An actor is capable of running logic, creating more actors, sending messages to other actors, and receiving messages.
  • No two actors are able to write to the same piece of shared memory, they are free to mutate their own memory.
  • An actor is like a function in a functional language, accepting inputs and avoiding access to global state.
  • Actors are single-threaded.
  • A system that uses actors should be resilient to delays and out-of-order delivery, especially since actors can be spread across a network.
  • Individual actors can also have the concept of an address. For example, tcp://127.0.0.1:1234/3 might refer to the third actor running in a program on the local computer listening on port 1234.
  • With the actor pattern, you shouldn’t think of the joined actors as external APIs. Instead, think of them as an extension of the program itself.

Refer to the chart from the book:

actor model

GitFlow & Blog Version Control

I’ve upgraded my blog theme recently. In my best practice in development process, I prefer using gitflow to manage the code. It is one of popular version control workflow on the planet. And the FLOW looks like this:

gitflow

In almost one year, I am always git push my posts on main branch. But before I start upgraded my theme, I git checkout to develop branch. According to the chart above, when we want to add some new feature, we create a FEATURE branch from develop branch. After finishing it, we merge the feature branch into develop branch. But how to bring my new feature deploying to my blog? (in here means main branch) We have to create a RELEASE branch, then close it, merging it into bothdevelop branch AND main branch. Release branch seems meaningless here, because it is used as testing the feature from QAs. Since this is my personal project, so release branch does not do anything. I just follow the gitflow.

In main branch, there is only one way to update new code: from release branch. But in my blog here, it might be pain. I am wandering, what if I created every post at develop branch, then must do tedious create-branch-merge-branch process every time I want to publish my new post.

From the beginning, I insisted on doing the right gitflow process. But now I must compromise. “That’s not practical.” I told to myself. So the conclusion is, I have decided do this into two parts.

First, creating posts will remain on main branch directly, for the sake of convenience. Second, other things will doing it on develop or feature branch. Something like updating blog config, making some change with folder structure, trying modified the theme, …etc.

I think it is good to go.

Cheers.