Combining Tailwind with Color Schemes for Stylish Text and Text Shadow Effects

tl;dr I have a utility class in my tailwind.config.ts for customizing text shadows' dimensions and colors. However, when using Tailwind Merge, I face conflicts between text-shadow-{size/color} and text-{color}.

The Issue

In CSS, text shadows are often useful for enhancing text designs or creating contrast without drop shadows. My custom text-shadow utility in Tailwind Config works well unless applied on a component with Tailwind Merge, causing confusion due to conflicts.

The Fix

To address this problem, I aimed to use extendTailwindMerge. Although the documentation is detailed, it lacks specific examples beyond foo, bar, and baz, making it challenging to implement bespoke solutions.

Seeking Help

I'm seeking advice on modifying my tailwind.config.ts and custom twMerge() function to resolve this issue. Any insights or suggestions would be greatly appreciated. Thank you!

The Code

// Updated code goes here

My Attempt

// Additional tweaks in progress

Illustrative Example

Input

// Illustrative scenario 

Expected Output

<a href='https://andrilla.net' class='text-blue-500 text-shadow-lg text-shadow-red-500'>Website</a>

Actual Result

<a href='https://andrilla.net' class='text-shadow-lg'>Website</a>

Answer №1

Error Detection

Per the documentation on class groups configuration:

The library utilizes the concept of class groups, which is an array of Tailwind classes that modify the same CSS property. For example, here is the position class group.

const positionClassGroup = ['static', 'fixed', 'absolute', 'relative', 'sticky']

tailwind-merge handles conflicts between classes in a class group and only retains the last one passed to the merge function call.

This implies that your text-shadow-<size> and text-shadow-<color> classes would override each other, not to mention the text-<color> class. Therefore, only the text-shadow-* class gets rendered (based on testing, it was the text-shadow-red-500 class since it was last in the twMerge() call).

In any case, your class group configuration is incorrect:

extend: {
  classGroups: {
    'text-shadow': [
      'sm',
      'DEFAULT',
      'md',
      'lg',

It actually registers the classes sm, DEFAULT, md, lg, etc., not text-shadow-sm, text-shadow, text-shadow-md, text-shadow-lg as might have been expected.

Instead, the initial key should represent the class group "ID", containing an array of objects where those keys can be a class name prefix with an array of values:

extend: {
  classGroups: {
    'text-shadow': [{ 'text-shadow': […] }]

Resolution

Further details from the same Class Groups documentation are provided:

Tailwind classes often share the beginning of the class name, allowing elements in a class group to be represented by an object with values following the same pattern as the class group (recursive shape). Within the object, each key combines with all elements in the corresponding array using a dash (-) in between.

For instance, consider the overflow class group resulting in classes like overflow-auto, overflow-hidden, overflow-visible, and overflow-scroll.

const overflowClassGroup = [{ overflow: ['auto', 'hidden', 'visible', 'scroll'] }]

Examples from the default configuration also demonstrate this pattern for shared prefixes:

'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] }],

Adopting this approach in our own configuration entails defining the first key as a group ID, encapsulating our class name specifications and dividing into two distinct class groups:

extend: {
  classGroups: {
    'text-shadow-size': [
      {
        'text-shadow': [
          'sm',
          '',
          'md',
          'lg',
          'xl',
          '2xl',
          '3xl',
          'none',
        ],
      },
    ],
    'text-shadow-color': [
      {
        'text-shadow': [
          ...colorList,
          'transparent',
          'white',
          'black',
        ],
      },
    ],
  },
},

Furthermore, replacing the 'DEFAULT' entry with '' is necessary. This is due to the fact that 'DEFAULT' would translate to text-shadow-DEFAULT, whereas the actual class name in Tailwind should be text-shadow.

// 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 hexadecimal alpha format
 * @param decimalAlpha
 * @returns
 */
function decimalAlphaToHex(decimalAlpha) {
  // Ensure the input falls 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()

  // Guarantee the hexadecimal value consumes two digits (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

...


Note that your text-shadow-<color> utilities define --tw-text-shadow-color CSS variables while your text-shadow-<size> utilities use --tw-text-shadow. Hence, no text shadow displays in the above preview, but you can inspect the elements to verify their attached class names.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Is there a way to properly align unordered lists (such as a navigation menu) within a div with a width set at

UPDATE: I found a creative solution to my issue. By adding margin-left and margin-right set to auto, along with fixing the width of the nav menu to 1002px and setting its positioning to relative, I was able to achieve the desired effect. Additionally, I cr ...

Refreshing React by injecting iframe with highest z-index after modifications (development)

I am facing an issue with my Create React App and its .env file setup as shown below: BROWSER=none SKIP_PREFLIGHT_CHECK=true INLINE_RUNTIME_CHUNK=false Whenever I run the app using yarn start, the server updates without refreshing the page or losing any s ...

Expand or collapse Angular Material Accordion depending on selected Radio button choice

Is it possible to use a mat-accordion with radio buttons where each panel expands only when its corresponding radio button is selected? I have the radio buttons functioning correctly, but the panels are expanding and collapsing with every click rather than ...

Develop a descriptive box for a radio button form using jQuery

I am working on creating a form with simple yes/no questions. If the answer is no, no explanation is needed. However, if the answer is yes, I want to insert a new table row and display a textarea for an explanation. To ensure data validation, I am utilizi ...

Do I have to include the sizes attribute when using w-descriptors in srcset?

After reading several articles discussing the use of srcset for device-pixel-density adjustments and art direction, I have come to a few conclusions: The State of Responsive Images in 2015 by Paddi Macdonnell srcset Part 2: The W descriptor and sizes att ...

Identification of inappropriate language in usernames

One of the challenges I'm facing is detecting inappropriate language in usernames. Currently, I am using regex to validate the username based on a specific pattern: "/^[A-Za-z0-9]*(\d*\.\d*)*[A-Za-z0-9]+$/" This regex pattern allows u ...

Utilizing jQuery to submit the form

After clicking the search icon, it displays an alert with the message ok, but not h. Based on the code snippet below, it is intended to display alerts for both ok and h. <?php if(isset($_POST['search'])){ echo "<script type='text/ ...

What is the best way to create a never-ending horizontal animation that moves a logo image from right

I have a dilemma with displaying my logo images. I have more than 10 logos, but I want to showcase only 6 of them on page load. The rest of the logos should slide horizontally from right to left in an infinite loop. Can anyone assist me with creating keyf ...

Creating a personalized message for a failed "Get" request using the HTTPS native module in combination with

Currently, I have implemented an Express application that includes an HTTP GET request to an external API. It functions properly when there are no errors; however, I am looking to customize the response sent to the client-side in case of failures: const ht ...

When text with delimiters is pasted into a Vuetify combobox, why aren't the chips separated correctly by the delimiters?

I'm attempting to create a Vuetify combobox with chips that automatically split the input based on predefined delimiters, such as ,. This means that if I paste the text a,b,c, the component should convert them into three separate chips: a, b, and c. H ...

Importing CSS properties from MUI v5 - A comprehensive guide

I'm working with several components that receive styles as props, such as: import { CSSProperties } from '@material-ui/styles/withStyles' // using mui v4 import because unsure how to import from v5 paths import { styled } from '@mui/mat ...

Setting the offset for panResponder with hooks: A step-by-step guide

While exploring a code example showcasing the use of panResponder for drag and drop actions in react native, I encountered an issue with item positioning. You can experiment with the code on this snack: The problem arises when dropping the item in the des ...

Troubleshooting a React Node.js Issue Related to API Integration

Recently, I started working on NodeJs and managed to create multiple APIs for my application. Everything was running smoothly until I encountered a strange issue - a new API that I added in the same file as the others is being called twice when accessed fr ...

Does the syntax for the $.ajax() function appear to be incorrect in this instance?

I am attempting to use .ajax to retrieve the source HTML of a specific URL (in this case, www.wikipedia.org) and insert it into the body of a document. However, the code provided below is not producing the expected outcome. <!DOCTYPE html> < ...

Troubleshooting React and React-Router Link Parameter Issues

I'm a newcomer to React and React Router, and I find myself struggling to grasp several concepts. Any insight or guidance you can provide would be greatly appreciated. I seem to be having trouble getting multiple parameters to work correctly. While I& ...

React Native: Issue with the data section in FlatList

I encountered an issue while using Flatlist to address a problem, but I ran into an error with the data property of my Flatlist. The error message is not very clear and I'm having trouble understanding it ( No overload matches this call. Overload 1 of ...

What is the reason behind getElementsByClassName not functioning while getElementById is working perfectly?

The initial code snippet is not functioning correctly DN.onkeyup = DN.onkeypress = function(){ var div = document.getElementById("DN").value document.document.getElementsByClassName("options-parameters-input").style.fontSize = div; } #one{ heigh ...

What is the best way to exchange configuration data among Javascript, Python, and CSS within my Django application?

How can I efficiently configure Javascript, Django templates, Python code, and CSS that all rely on the same data? For example, let's say I have a browser-side entry widget written in Javascript that interacts with an embedded Java app. Once the user ...

Steps for adjusting the length in the getRangeLabel function of mat paginator

@Injectable() export class MyCustomPaginatorIntl extends MatPaginatorIntl { public getRangeLabel = (page: number, pageSize: number, length: number): string => { if (length === 0 || pageSize === 0) { return `${ ...

Poorly formatted mui table representation

I have utilized the Table component from @mui/material and referenced the documentation at https://mui.com/material-ui/react-table/. To reproduce the issue, I have created a static table with fixed values. Here is the code snippet: <TableCo ...