Skip to content

Instantly share code, notes, and snippets.

@tafaust
Last active January 5, 2023 14:17
Show Gist options
  • Save tafaust/345830247456980d1c8ac6e53a2dd879 to your computer and use it in GitHub Desktop.
Save tafaust/345830247456980d1c8ac6e53a2dd879 to your computer and use it in GitHub Desktop.
Generates

Problem statement

You have a Next.JS project with tailwindcss.
You want to build a component that receives your custom color names via props

export type Props = {
  // […]
  color?: 'primary' | 'secondary' | 'success' | 'error' | 'warning';
  // […]
};

and should colorize the component accordingly in a dynamic fashion.

Your component is now supposed to build css class names on the fly but tailwindcss will NOT pick these up because they do not compile your code but merely run regular expressions for every static file specified in the contents array in the tailwind.config.js. (source: https://tailwindcss.com/docs/content-configuration#configuring-source-paths)

Solutions

There exist two solutions to this problem and both have their caveats.

1. Writing a color lookup table

You write a function that takes your dynamic color name - e.g. primary, secondary, and so on - and return the bare css class string. Make sure to have this file in your contents array so that tailwind can pick up these classes.
The caveats are fairly obvious with this approach:

  1. The method explodes in length due to the permutation set of class-combinations: $\text{pseudo}\times\text{prefix}\times\text{colorName}\times\text{intensity}$
    • You can use gen-tailwind-cmap.js to generate a function to lookup your dynamic css class names. Run it with
  2. Tailwind will end up generating all classes in your final bundle.

Optimally, you want to be concise and only list the classes you really need in your components to circumvent the caveats at the cost of development speed. See https://stackoverflow.com/a/69687962/2402281 as a reference.

2. Safelisting your classes

By safelisting, you force tailwindcss to pick up the dynamic classnames which are not in your static code files. The caveat here is that this might bloat your bundle with non-existent css classes that tailwind has to manage. From the docs:

For the smallest file size and best development experience, we highly recommend relying on your content configuration to tell Tailwind which classes to generate as much as possible. Safelisting is a last-resort, and should only be used in situations where it’s impossible to scan certain content for class names. These situations are rare, and you should almost never need this feature. If you need to make sure Tailwind generates certain class names that don’t exist in your content files, use the safelist option:

and

One example of where this can be useful is if your site displays user-generated content and you want users to be able to use a constrained set of Tailwind classes in their content that might not exist in your own site’s source files.

Source: https://tailwindcss.com/docs/content-configuration#safelisting-classes


Keywords: #tailwind #dynamic #class #names #className

const fs = require('fs');
const pseudo = ['hover', 'active', 'focus']
const prefix = ['text', 'bg'];
const colors = ['primary', 'secondary' , 'success' , 'error' , 'warning'];
const intensity = ['50' , '100' , '200' , '300' , '400' , '500' , '600' , '700' , '800' , '900'];
const fileContent = [];
const p = (str) => fileContent.push(str);
p('/* eslint-disable */');
p('// This file has been auto-generated! Do not modify directly!');
p('\n');
p('export type ThemeColor = {');
p(` colorName: '${colors.join("' | '")}';`);
p(` pseudo?: '${pseudo.join("' | '")}';`);
p(` prefix?: '${prefix.join("' | '")}';`);
p(` intensity?: '${intensity.join("' | '")}';`);
p('};');
p('\n');
p('/**');
p(' * Method is needed because of {@link https://tailwindcss.com/docs/content-configuration#dynamic-class-names}.');
p(' */');
p('export const pickColor = ({ colorName, prefix, pseudo, intensity }: ThemeColor) => {');
p(' if (intensity === undefined && pseudo === undefined && prefix === undefined) {');
p(` switch (colorName) {`);
for (const color of colors) {
p(` case '${color}':`);
p(` return '${color}';`);
}
p(` default: return 'primary';`);
p(` }`);
p(' } else if (intensity !== undefined && pseudo === undefined && prefix === undefined) {');
p(` switch (colorName) {`);
for (const color of colors) {
p(` case '${color}':${color === 'primary' ? ' default: ': ''}`);
p(` switch (intensity) {`);
for (const i of intensity) {
p(` case '${i}':`);
p(` return '${color}-${i}';`);
}
p(` default: return 'primary'; // we can naively return primary here due to the DEFAULT in the color map`);
p(` }`);
}
p(` }`);
p(' } else if (intensity === undefined && pseudo === undefined && prefix !== undefined) {');
p(` switch (colorName) {`);
for (const color of colors) {
p(` case '${color}':${color === 'primary' ? ' default: ': ''}`);
p(` switch (prefix) {`);
for (const pre of prefix) {
p(` case '${pre}':`);
p(` return '${pre}-${color}';`);
}
p(` default: throw new Error(\`Runtime error: there is no prefix "\${prefix}"!\`);`);
p(` }`);
}
p(` }`);
p(' } else if (intensity === undefined && pseudo !== undefined && prefix === undefined) {');
p(` switch (colorName) {`);
for (const color of colors) {
p(` case '${color}':${color === 'primary' ? ' default: ': ''}`);
p(` switch (pseudo) {`);
for (const pseu of pseudo) {
p(` case '${pseu}':`);
p(` return '${pseu}:${color}';`);
}
p(` default: throw new Error(\`Runtime error: there is no pseudo "\${pseudo}"!\`);`);
p(` }`);
}
p(` }`);
p(' } else if (intensity !== undefined && pseudo !== undefined && prefix === undefined) {');
p(` switch (colorName) {`);
for (const color of colors) {
p(` case '${color}':${color === 'primary' ? ' default: ': ''}`);
p(` switch (intensity) {`);
for (const i of intensity) {
p(` case '${i}':`);
p(` switch (pseudo) {`);
for (const pseu of pseudo) {
p(` case '${pseu}':`);
p(` return '${pseu}:${color}-${i}';`);
}
p(` default: throw new Error(\`Runtime error: there is no pseudo "\${pseudo}"!\`);`);
p(` }`);
}
p(` default:`);
p(` switch (pseudo) {`);
for (const pseu of pseudo) {
p(` case '${pseu}':`);
p(` return '${pseu}:${color}';`);
}
p(` default: throw new Error(\`Runtime error: there is no pseudo "\${pseudo}" for unknown intensity "\${intensity}"!\`);`);
p(` }`);
p(` }`);
}
p(` }`);
p(' } else if (intensity !== undefined && pseudo === undefined && prefix !== undefined) {');
p(` switch (colorName) {`);
for (const color of colors) {
p(` case '${color}':${color === 'primary' ? ' default: ': ''}`);
p(` switch (intensity) {`);
for (const i of intensity) {
p(` case '${i}':`);
p(` switch (prefix) {`);
for (const pre of prefix) {
p(` case '${pre}':`);
p(` return '${pre}-${color}-${i}';`);
}
p(` default: throw new Error(\`Runtime error: there is no prefix "\${prefix}"!\`);`);
p(` }`);
}
p(` default: throw new Error(\`Runtime error: there is no prefix "\${prefix}" for unknown intensity "\${intensity}"!\`);`);
p(` }`);
}
p(` }`);
p(' } else if (intensity === undefined && pseudo !== undefined && prefix !== undefined) {');
p(` switch (colorName) {`);
for (const color of colors) {
p(` case '${color}':${color === 'primary' ? ' default: ': ''}`);
p(` switch (prefix) {`);
for (const pre of prefix) {
p(` case '${pre}':`);
p(` switch (pseudo) {`);
for (const pseu of pseudo) {
p(` case '${pseu}':`);
p(` return '${pseu}:${pre}-${color}';`);
}
p(` default: throw new Error(\`Runtime error: there is no pseudo "\${pseudo}"!\`);`);
p(` }`);
}
p(` default: throw new Error(\`Runtime error: there is no pseudo "\${pseudo}" for unknown prefix "\${prefix}"!\`);`);
p(` }`);
}
p(` }`);
p(' } else if (intensity !== undefined && pseudo !== undefined && prefix !== undefined) {');
p(` switch (colorName) {`);
for (const color of colors) {
p(` case '${color}':${color === 'primary' ? ' default: ': ''}`);
p(` switch (intensity) {`);
for (const i of intensity) {
p(` case '${i}':`);
p(` switch (pseudo) {`);
for (const pseu of pseudo) {
p(` case '${pseu}':`);
p(` switch (prefix) {`);
for (const pre of prefix) {
p(` case '${pre}':`);
p(` return '${pseu}:${pre}-${color}-${i}';`);
}
p(` default: throw new Error(\`Runtime error: there is no prefix "\${prefix}"!\`);`);
p(` }`);
}
p(` default: throw new Error(\`Runtime error: there is no prefix "\${prefix}" for unknown pseudo "\${pseudo}"!\`);`);
p(` }`);
}
p(` default: throw new Error(\`Runtime error: there is no prefix "\${prefix}" and pseudo "\${pseudo}" for unknown intensity "\${intensity}"!\`);`);
p(` }`);
}
p(` }`);
p(` }`);
p('};');
p('');
const generatedFile = fileContent.join('\n');
fs.writeFileSync('./gen-tailwind-colormap.ts', generatedFile);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment