tl;dr 我的
text-shadow
中有一个 tailwind.config.ts
实用程序类,它允许我更改阴影的尺寸和颜色。我正在使用 Tailwind Merge,我不知道如何阻止 text-shadow-{size/color}
与 text-{color}
发生冲突。
通常在 CSS 中,使用文本阴影对于在文本周围添加很酷的设计甚至添加文本对比度非常有帮助,而不是使用投影。不久前,我为我的 Tailwind Config 创建了一个
text-shadow
实用程序类,它工作得很好,直到我在利用 Tailwind Merge 的组件上使用它。 Tailwind Merge 是一个很棒的包,但是当使用自定义实用程序类时,它可能会感到困惑。
当然,我的目标是使用
extendTailwindMerge
来纠正这个问题。关于配置 Tailwind Merge 的文档非常详细,但由于它只提供了 foo
、bar
和 baz
作为示例,我有点困惑如何做特定的事情。
请查看我的
tailwind.config.ts
和自定义 twMerge()
功能,如果您有任何想法,请告诉我。谢谢!
// 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
// utils/custom-tailwind-merge.ts
import { extendTailwindMerge } from 'tailwind-merge'
import colors from 'tailwindcss/colors'
const colorList: { [key: string]: string[] }[] = []
const excludedColors = [
'black',
'blueGray',
'coolGray',
'inherit',
'transparent',
'trueGray',
'warmGray',
'white',
]
Object.entries(colors).forEach(([colorName, valueList]) => {
if (excludedColors.includes(colorName)) 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, href }: LinkProps) {
const defaultClasses = 'text-blue-500'
return (
<a href={href} className={twMerge(defaultClasses, className)}>{children}</a>
)
}
import Link from '@/components/link'
export default function Page() {
return (
<Link href='https://andrilla.net' className='text-shadow-lg text-shadow-red-500'>Website</Link>
)
}
<a href='https://andrilla.net' class='text-blue-500 text-shadow-lg text-shadow-red-500'>Website</a>
<a href='https://andrilla.net' class='text-shadow-lg'>Website</a>
根据有关班级组配置的文档:
该库使用“类组”的概念,它是一组 Tailwind 类,它们都修改相同的 CSS 属性。例如。这是职位类别组。
const positionClassGroup = ['static', 'fixed', 'absolute', 'relative', 'sticky']
tailwind-merge 解决类组中的类之间的冲突,并且仅保留传递给合并函数调用的最后一个。这意味着您的
text-shadow-<size>
和
text-shadow-<color>
类将取消相互覆盖,更不用说 text-<color>
类了。因此,为什么只渲染 text-shadow-*
类(根据我的测试,它是 text-shadow-red-500
类,因为它是 twMerge()
调用中的最后一个)。无论如何,您的类组配置无论如何都是不正确的,因为第一个键是类组“ID”,它应该是一个对象数组,其中那些键可以是带有值数组的类名前缀:
extend: {
classGroups: {
'text-shadow': [{ 'text-shadow': […] }]
解决方案
Tailwind 类通常共享类名的开头,因此类组中的元素也可以是具有与类组相同形状的值的对象(是的,形状是递归的)。在对象中,每个键与相应数组中的所有元素相连,中间有破折号 (
默认配置-
)。
例如这是溢出类组,导致类overflow-auto
、
、overflow-hidden
和overflow-visible
。overflow-scroll
const overflowClassGroup = [{ overflow: ['auto', 'hidden', 'visible', 'scroll'] }]
以及来自
的一些示例,它们共享相同的前缀:
'font-size': [{ text: ['base', isTshirtSize, isArbitraryLength] }],
// …
'text-alignment': [{ text: ['left', 'center', 'right', 'justify', 'start', 'end'] }],
// …
'text-color': [{ text: [colors] }],
'font-weight': [
{
font: [
'thin',
'extralight',
// …
'font-family': [{ font: [isAny] }],
我们可以在自己的配置中遵循相同的模式,其中第一个键是某个组 ID,该值包含我们的类名称规范并分为两个单独的类组:
extend: {
classGroups: {
'text-shadow-size': [
{
'text-shadow': [
'sm',
'DEFAULT',
'md',
'lg',
'xl',
'2xl',
'3xl',
'none',
],
},
],
'text-shadow-color': [
{
'text-shadow': [
...colorList,
'transparent',
'white',
'black',
],
},
],
},
},
//// tailwind.config.ts
const flattenColorPalette = (colors)=>Object.assign({}, ...Object.entries(colors !== null && colors !== void 0 ? colors : {}).flatMap(([color, values])=>typeof values == "object" ? Object.entries(flattenColorPalette(values)).map(([number, hex])=>({
[color + (number === "DEFAULT" ? "" : `-${number}`)]: hex
})) : [
{
[`${color}`]: values
}
]));
/**
* ### Decimal Alpha to HEX
* - Converts an RGB decimal alpha value to hexidecimal alpha format
* @param decimalAlpha
* @returns
*/
function decimalAlphaToHex(decimalAlpha) {
// 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
}
}
tailwind.config = {
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: [
tailwind.plugin(function ({ matchUtilities, theme }) {
const colors = {},
opacities = 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 = {}
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 } }
)
}),
],
}
//// utils/custom-tailwind-merge.ts
const { colors } = tailwind;
const { extendTailwindMerge } = tailwindMerge;
const colorList = [];
const excludedColors = [
'black',
'blueGray',
'coolGray',
'inherit',
'transparent',
'trueGray',
'warmGray',
'white',
]
Object.entries(colors).forEach(([colorName, valueList]) => {
if (excludedColors.includes(colorName)) return
colorList.push({ [colorName]: Object.keys(valueList) })
})
const twMerge_ = extendTailwindMerge({
extend: {
classGroups: {
'text-shadow-size': [
{
'text-shadow': [
'sm',
'DEFAULT',
'md',
'lg',
'xl',
'2xl',
'3xl',
'none',
],
},
],
'text-shadow-color': [
{
'text-shadow': [
...colorList,
'transparent',
'white',
'black',
],
},
],
},
},
})
//// Link.ts
function Link({ children, className, href }) {
const defaultClasses = 'text-blue-500'
return (
<a href={href} className={twMerge_(defaultClasses, className)}>{children}</a>
)
}
//// Input
ReactDOM.createRoot(document.getElementById('app')).render(
<React.Fragment>
<Link href='https://andrilla.net' className='text-shadow-lg text-shadow-red-500'>Website</Link>
<Link href='' className='text-shadow-lg text-shadow-red-500 text-shadow-blue-200 text-shadow-md text-red-700'>Website</Link>
</React.Fragment>
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js" integrity="sha512-QVs8Lo43F9lSuBykadDb0oSXDL/BbZ588urWVCRwSIoewQv/Ewg1f84mK3U790bZ0FfhFa1YSQUmIhG+pIRKeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js" integrity="sha512-6a1107rTlA4gYpgHAqbwLAtxmWipBdJFcq8y5S/aTge3Bp+VAklABm2LO+Kg51vOWR9JMZq1Ovjl5tpluNpTeQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.tailwindcss.com/3.4.3"></script>
<script>window.exports = {}</script>
<script src="https://unpkg.com/[email protected]/dist/bundle-cjs.js"></script>
<script>window.tailwindMerge = window.exports; window.exports = {}</script>
<div id="app"></div>