tl;dr I have a text-shadow
utility class in my tailwind.config.ts
that allows me to change both the dimensions and colors of the shadow. I’m using Tailwind Merge, I can’t figure out how to stop text-shadow-{size/color}
from conflicting with text-{color}
.
The Problem
Often in CSS, using text shadows is very helpful for adding cool designs around text or even adding contrast for the text, rather than using a drop shadow. I’ve been created a text-shadow
utility class to my Tailwind Config a while ago, and it works great, until I use it on a component that utilizes Tailwind Merge. Tailwind Merge is a great package, but—when using custom utility classes—it can get confused.
The Solution
Naturally, my goal was to use extendTailwindMerge
to correct this issue. The documentation on configuring Tailwind Merge is well detailed, but since it only gives foo
, bar
, and baz
for examples, I was a bit confused how to do something specific.
The Ask
Please look at my tailwind.config.ts
and custom twMerge()
function and let me know if you have any ideas. Thanks!
The Code
// tailwind.config.ts
// * Types
import type { Config } from 'tailwindcss'
import type { CSSRuleObject, ThemeConfig } from 'tailwindcss/types/config'
/**
* ### Decimal Alpha to HEX
* - Converts an RGB decimal alpha value to hexidecimal alpha format
* @param decimalAlpha
* @returns
*/
export function decimalAlphaToHex(decimalAlpha: number): string {
// Ensure the input is within the valid range
if (decimalAlpha < 0 || decimalAlpha > 1)
throw new Error('Decimal alpha value must be between 0 and 1')
// Convert decimal alpha to a hexadecimal value
const alphaHex = Math.floor(decimalAlpha * 255)
.toString(16)
.toUpperCase()
// Ensure the hexadecimal value is two digits long (e.g., 0A instead of A)
if (alphaHex.length < 2) {
return '0' + alphaHex
} else {
return alphaHex
}
}
type GetTheme = <
TDefaultValue =
| Partial<
ThemeConfig & {
extend: Partial<ThemeConfig>
}
>
| undefined,
>(
path?: string | undefined,
defaultValue?: TDefaultValue | undefined,
) => TDefaultValue
// * Plugins
import plugin from 'tailwindcss/plugin'
import headlessui from '@headlessui/tailwindcss'
// @ts-ignore
import { default as flattenColorPalette } from 'tailwindcss/lib/util/flattenColorPalette'
const config: Config = {
content: [
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
textShadow: {
sm: '0 0 0.125rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))',
DEFAULT: '0 0 0.25rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))',
md: '0 0 0.5rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))',
lg: '0 0 0.75rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))',
xl: '0 0 1rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))',
'2xl': '0 0 2rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))',
'3xl': '0 0 3rem var(--tw-text-shadow, hsl(0 0% 0% / 0.25))',
none: 'none',
},
},
plugins: [
plugin(function ({ matchUtilities, theme }) {
const colors: { [key: string]: string } = {},
opacities: { [key: string]: string } = flattenColorPalette(
theme('opacity'),
),
opacityEntries = Object.entries(opacities)
Object.entries(flattenColorPalette(theme('colors'))).forEach((color) => {
const [key, value] = color
if (typeof key !== 'string' || typeof value !== 'string') return null
colors[key] = value.replace(' / <alpha-value>', '')
if (value.startsWith('#') && value.length === 7)
opacityEntries.forEach(([opacityKey, opacityValue]) => {
colors[`${key}/${opacityKey}`] = `${value}${decimalAlphaToHex(
Number(opacityValue),
)}`
})
if (value.startsWith('#') && value.length === 4)
opacityEntries.forEach(([opacityKey, opacityValue]) => {
colors[`${key}/${opacityKey}`] = `${value}${value.slice(
1,
)}${decimalAlphaToHex(Number(opacityValue))}`
})
if (value.startsWith('rgb') || value.startsWith('hsl'))
opacityEntries.forEach(([opacityKey, opacityValue]) => {
colors[`${key}/${opacityKey}`] = `${value.slice(
0,
-1,
)} / ${opacityValue})`.replace(' / <alpha-value>', '')
})
})
matchUtilities(
{
'text-shadow': (value) => {
const cssProperties: CSSRuleObject = {}
if (
typeof value === 'string' &&
(value.startsWith('#') ||
value.startsWith('rgb') ||
value.startsWith('hsl'))
) {
cssProperties['--tw-text-shadow-color'] = value
} else {
cssProperties['text-shadow'] = value
}
return cssProperties
},
},
{ values: { ...theme('textShadow'), ...colors } },
)
}),
],
}
export default config
My Best Attempt
// utils/custom-tailwind-merge.ts
import { extendTailwindMerge } from 'tailwind-merge'
import colors from 'tailwindcss/colors'
const colorList: { [key: string]: string[] }[] = []
Object.entries(colors).forEach(([colorName, valueList]) => {
if (
colorName === 'inherit' ||
colorName === 'transparent' ||
colorName === 'white' ||
colorName === 'black'
)
return
return colorList.push({ [colorName]: Object.keys(valueList) })
})
type AdditionalClassGroupIds = 'text-shadow'
export const twMerge = extendTailwindMerge<AdditionalClassGroupIds>({
extend: {
classGroups: {
'text-shadow': [
'sm',
'DEFAULT',
'md',
'lg',
'xl',
'2xl',
'3xl',
'none',
...colorList,
'transparent',
'white',
'black',
],
},
},
})
// components/link.tsx
import type { LinkProps } from '@/typings/components'
import { twMerge } from '@/utils/custom-tailwind-merge'
export default function Link({ children, className }: LinkProps) {
const defaultClasses = 'text-blue-500'
return (
<a className={twMerge(defaultClasses, className)}>{children}</a>
)
}