TailwindCSS v4.0 and dynamic/autonomous extendTailwindMerge. #532
-
I have a really complex tailwind config, so I had to create a custom extendTailwindMerge theme: {
DEFAULT: 'var(--theme)',
hover: 'var(--theme-hover)',
content: 'var(--theme-content)',
'content-hover': 'var(--theme-content-hover)',
// Inverted
inverted: 'var(--theme-inverted)',
'inverted-hover': 'var(--theme-inverted-hover)',
'inverted-content': 'var(--theme-inverted-content)',
// Gradient
'gradient-start': 'var(--theme-gradient-start)',
'gradient-end': 'var(--theme-gradient-end)',
// Accent
accent: 'var(--theme-accent)',
'accent-hover': 'var(--theme-accent-hover)',
},
// System
primary: {
DEFAULT: '#0C0C0C',
content: '#FFFFFF',
stroke: '#2D2D2D',
},
active: {
content: '#85F9BD',
'content-hover': '#68DCA2',
stroke: '#214F4B',
'stroke-hover': '#68DCA2',
},
... What I used to do in v3In V3 as long as config was in JS. I was able to create a import resolveConfig from 'tailwindcss/resolveConfig';
import tailwindConfig from './tailwind.config';
const fullConfig = resolveConfig(tailwindConfig);
declare module 'tailwind.config' {
export default fullConfig;
}
export default fullConfig; and so here my tailwind merge import { extendTailwindMerge, twJoin } from 'tailwind-merge';
import tailwindConfig from 'tailwind.config';
type SubKeys = Record<string, string | SubKeys>;
const ARBITRARY_VALUE = (classPart: string) => /^\[.+\]$/.test(classPart);
const getSubKeys = (obj: SubKeys, prefix: string = ''): string[] =>
Object.keys(obj)
.map((key) => {
if (typeof obj[key] === 'string') {
if (key === 'DEFAULT') {
return prefix;
}
if (prefix) {
return `${prefix}-${key}`;
}
return key;
}
let nextPrefix;
if (prefix) {
nextPrefix = key === 'DEFAULT' ? prefix : `${prefix}-${key}`;
} else {
nextPrefix = key === 'DEFAULT' ? '' : key;
}
return getSubKeys(obj[key], nextPrefix);
})
.flat();
const getKeys = (obj: Record<string, string | [string, string]>): string[] =>
Object.keys(obj).map((key) => {
if (key === 'DEFAULT') return '';
return key;
});
type AdditionalClassGroupIds = never;
type AdditionalThemeGroupIds = never;
export const mergeClass = extendTailwindMerge<
AdditionalClassGroupIds,
AdditionalThemeGroupIds
>({
override: {
theme: {
colors: [...getSubKeys(tailwindConfig.theme.colors), ARBITRARY_VALUE],
},
classGroups: {
'font-size': [
{
text: [
...getKeys(tailwindConfig.theme.fontSize),
ARBITRARY_VALUE,
'current',
'transparent',
],
},
],
gap: [
{ gap: [...getKeys(tailwindConfig.theme.spacing), ARBITRARY_VALUE] },
],
p: [{ p: [...getKeys(tailwindConfig.theme.spacing), ARBITRARY_VALUE] }],
px: [{ px: [...getKeys(tailwindConfig.theme.spacing), ARBITRARY_VALUE] }],
py: [{ py: [...getKeys(tailwindConfig.theme.spacing), ARBITRARY_VALUE] }],
pt: [{ pt: [...getKeys(tailwindConfig.theme.spacing), ARBITRARY_VALUE] }],
pr: [{ pr: [...getKeys(tailwindConfig.theme.spacing), ARBITRARY_VALUE] }],
pb: [{ pb: [...getKeys(tailwindConfig.theme.spacing), ARBITRARY_VALUE] }],
pl: [{ pl: [...getKeys(tailwindConfig.theme.spacing), ARBITRARY_VALUE] }],
m: [{ m: [...getKeys(tailwindConfig.theme.spacing), ARBITRARY_VALUE] }],
mx: [{ mx: [...getKeys(tailwindConfig.theme.spacing), ARBITRARY_VALUE] }],
my: [{ my: [...getKeys(tailwindConfig.theme.spacing), ARBITRARY_VALUE] }],
mt: [{ mt: [...getKeys(tailwindConfig.theme.spacing), ARBITRARY_VALUE] }],
mr: [{ mr: [...getKeys(tailwindConfig.theme.spacing), ARBITRARY_VALUE] }],
mb: [{ mb: [...getKeys(tailwindConfig.theme.spacing), ARBITRARY_VALUE] }],
ml: [{ ml: [...getKeys(tailwindConfig.theme.spacing), ARBITRARY_VALUE] }],
w: [{ w: [...getKeys(tailwindConfig.theme.width), ARBITRARY_VALUE] }],
h: [{ h: [...getKeys(tailwindConfig.theme.height), ARBITRARY_VALUE] }],
'min-h': [
{ 'min-h': [...getKeys(tailwindConfig.theme.width), ARBITRARY_VALUE] },
],
'min-w': [
{ 'min-w': [...getKeys(tailwindConfig.theme.height), ARBITRARY_VALUE] },
],
'max-h': [
{ 'max-h': [...getKeys(tailwindConfig.theme.width), ARBITRARY_VALUE] },
],
'max-w': [
{ 'max-w': [...getKeys(tailwindConfig.theme.height), ARBITRARY_VALUE] },
],
rounded: [{ rounded: getKeys(tailwindConfig.theme.borderRadius) }],
'border-w': [{ border: getKeys(tailwindConfig.theme.borderWidth) }],
'outline-w': [{ border: getKeys(tailwindConfig.theme.outlineWidth) }],
ease: [{ ease: getKeys(tailwindConfig.theme.transitionTimingFunction) }],
},
},
});
export const joinClass = twJoin; This prevents me from typing everything each time I add a color. Tailwind v4.0, no more JSAs long as the new way to do tailwind is to add everything in the CSS @theme {
--color-*: initial;
--color-transparent: transparent;
--color-current: currentColor;
... I tried to do get the CSS from the document directly, and pars the variables function getCSSVariables(key: string) {
const root = document.documentElement;
const styles = getComputedStyle(root);
const variables= {};
// Iterate over all properties in the computed styles
for (let i = 0; i < styles.length; i++) {
const propertyName = styles[i];
if (propertyName.startsWith(`--${key}`)) {
const value = styles.getPropertyValue(propertyName).trim()
colorVariables[propertyName] = value;
}
}
...
} but tailwind variables are not in Do you have a solution with prevent me from doing the following ? And moreover, that maintain itself automatically ?const ARBITRARY_VALUE = (value: string) =>
/^\[(?:(\w[\w-]*):)?(.+)\]$/i.test(value);
const ARBITRARY_VARIABLE = (value: string) =>
/^\[.+\]$/.test(value) || /^\[.+\]$/.test(value);
const FRACTION = (value: string) => /^\d+\/\d+$/.test(value);
const COLOR_VALUES = [
'transparent',
'current',
...
ARBITRARY_VALUE,
];
const TEXT_VALUES = [
...
];
const SPACING_VALUES = [
'auto',
...
fromTheme('spacing'),
FRACTION,
ARBITRARY_VARIABLE,
ARBITRARY_VALUE,
];
const RADIUS_VALUES = [
...
ARBITRARY_VARIABLE,
];
const EASE_VALUES = [
'linear',
...
ARBITRARY_VARIABLE,
ARBITRARY_VALUE,
];
export const mergeClass = extendTailwindMerge({
override: {
theme: {
color: COLOR_VALUES,
},
classGroups: {
'font-size': [{ text: TEXT_VALUES }],
// Spacing utilities using CSS spacing variables
gap: [{ gap: SPACING_VALUES }],
p: [{ p: SPACING_VALUES }],
....
// Size utilities
w: [{ w: SPACING_VALUES }],
h: [{ h: SPACING_VALUES }],
....
// Border radius using CSS radius variables
rounded: [{ rounded: RADIUS_VALUES }],
'rounded-s': [{ 'rounded-s': RADIUS_VALUES }],
'rounded-e': [{ 'rounded-e': RADIUS_VALUES }],
'rounded-t': [{ 'rounded-t': RADIUS_VALUES }],
'rounded-r': [{ 'rounded-r': RADIUS_VALUES }],
...
// Transition timing using CSS ease variables
ease: [{ ease: EASE_VALUES }],
},
},
});
export const joinClass = twJoin; |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 4 replies
-
Hey @quentinjeanningros! 👋 My general recommendation is to copy over the values. Usually this isn't a problem because people don't change their Tailwind config so much. In Tailwind CSS v4 you also can use the export const twMerge = extendTailwindMerge({
extend: {
theme: {
// Added CSS variables --text-my-text-size-1 and --text-my-text-size-2
text: ['my-text-size-1', 'my-text-size-2'],
// Added CSS variables --spacing-my-spacing-1 and --spacing-my-spacing-2
spacing: ['my-spacing-1', 'my-spacing-2'],
// …
}
}
}) By the way you don't need to define your colors in tailwind-merge. tailwind-merge already falls back to color classes for unknown classes that could be a color class, like If your Tailwind config changes often, you'll have to find your own approach. It's quite the bummer that Tailwind seems to treat the JS config files as a second class config. Maybe you can build a JS config that generates the CSS config file via codegen? Then you could use that code to create the tailwind-merge config as well. |
Beta Was this translation helpful? Give feedback.
Hey @quentinjeanningros! 👋
My general recommendation is to copy over the values. Usually this isn't a problem because people don't change their Tailwind config so much. In Tailwind CSS v4 you also can use the
theme
for all the CSS variables and don't need to use class groups for those (more in the docs).B…