10. Qué es y cómo utiliza Tailwind Merge

 

Tailwind Merge - Combinación Inteligente de Clases CSS

1. Introducción a Tailwind Merge

1.1. ¿Qué es Tailwind Merge?

Tailwind Merge es una utilidad que permite combinar cadenas de clases de Tailwind CSS y resolver conflictos automáticamente.

Problema que resuelve:

vue

<!-- Clases base del componente -->

<button class="bg-blue-500 text-white px-4 py-2 rounded">


<!-- Queremos sobrescribir -->

<button class="bg-blue-500 text-white px-4 py-2 rounded bg-red-500">

<!-- ❌ Conflicto: ¿azul o rojo? -->

Solución con Tailwind Merge:

vue

<button :class="twMerge('bg-blue-500 text-white px-4 py-2 rounded', 'bg-red-500')">

<!-- ✅ Resultado: bg-red-500 (prevalece la última) -->

1.2. Características Principales

  • ✅ Resuelve conflictos automáticamente

  • ✅ Maneja variantes (hover:, dark:, etc.)

  • ✅ Ligero - Solo 3KB

  • ✅ TypeScript - Soporte nativo


2. Instalación

2.1. Instalar Dependencia

bash

npm install --save-dev tailwind-merge

2.2. Verificar Instalación

json

// package.json

"devDependencies": {

    "tailwind-merge": "^2.0.0"

}


3. Crear Componente de Botón Mejorado

3.1. Componente PrimaryButton Mejorado

Modificar resources/js/Components/PrimaryButton.vue:

vue

<script setup>

import { computed } from 'vue'

import { Link } from '@inertiajs/vue3'

import { twMerge } from 'tailwind-merge'


// Props del componente

const props = defineProps({

    href: {

        type: String,

        default: ''

    },

    class: {

        type: String,

        default: ''

    },

    ariaLabel: {

        type: String,

        required: true

    }

})


// Clases base del componente

const defaultClasses = [

    'inline-flex',

    'items-center',

    'px-4',

    'py-2',

    'bg-blue-600',

    'border',

    'border-transparent',

    'rounded-md',

    'font-semibold',

    'text-xs',

    'text-white',

    'uppercase',

    'tracking-widest',

    'hover:bg-blue-700',

    'focus:bg-blue-700',

    'active:bg-blue-900',

    'focus:outline-none',

    'focus:ring-2',

    'focus:ring-blue-500',

    'focus:ring-offset-2',

    'transition',

    'ease-in-out',

    'duration-150',

    'dark:bg-blue-600',

    'dark:hover:bg-blue-700',

    'dark:focus:bg-blue-700',

    'dark:active:bg-blue-900',

    'dark:focus:ring-blue-400',

    'dark:focus:ring-offset-gray-800'

].join(' ')


// Clases finales combinadas

const classes = computed(() => {

    return twMerge(defaultClasses, props.class)

})

</script>


<template>

    <!-- Si hay href, renderizar Link -->

    <Link

        v-if="href"

        :href="href"

        :class="classes"

        :aria-label="ariaLabel"

    >

        <slot />

    </Link>

    

    <!-- Si no hay href, renderizar botón -->

    <button

        v-else

        :class="classes"

        :aria-label="ariaLabel"

    >

        <slot />

    </button>

</template>


4. Uso del Componente Mejorado

4.1. Como Botón Normal

vue

<PrimaryButton aria-label="Crear chatbot">

    Crear Chatbot

</PrimaryButton>

4.2. Como Link de Inertia

vue

<PrimaryButton 

    href="/chatbots/1" 

    aria-label="Ver chatbot"

>

    Ver Chatbot

</PrimaryButton>

4.3. Sobrescribiendo Clases

vue

<!-- Texto en rojo -->

<PrimaryButton 

    href="/chatbots/1" 

    class="text-red-500 hover:text-red-700"

    aria-label="Ver chatbot"

>

    Ver Chatbot

</PrimaryButton>


<!-- Fondo verde -->

<PrimaryButton 

    href="/chatbots/create" 

    class="bg-green-600 hover:bg-green-700"

    aria-label="Crear chatbot"

>

    Crear Chatbot

</PrimaryButton>


5. Funcionalidad de Tailwind Merge

5.1. Resolución de Conflictos

javascript

import { twMerge } from 'tailwind-merge'


// Ejemplos de resolución

twMerge('px-2 py-1', 'px-4')        // 'py-1 px-4'

twMerge('bg-red-500', 'bg-blue-500') // 'bg-blue-500'

twMerge('text-sm text-gray-500', 'text-lg') // 'text-lg text-gray-500'

5.2. Variantes y Modificadores

javascript

// Variantes funcionan correctamente

twMerge('hover:bg-red-500', 'hover:bg-blue-500') 

// 'hover:bg-blue-500'


// Dark mode

twMerge('dark:bg-gray-800', 'dark:bg-gray-900')

// 'dark:bg-gray-900'


// Múltiples estados

twMerge('focus:ring-2 focus:ring-blue-500', 'focus:ring-red-500')

// 'focus:ring-2 focus:ring-red-500'

5.3. Clases sin Conflicto

javascript

// Se combinan

twMerge('p-4', 'm-2')        // 'p-4 m-2'

twMerge('flex', 'items-center') // 'flex items-center'

twMerge('w-full', 'h-auto')   // 'w-full h-auto'


6. Mejoras Adicionales

6.1. Extraer Lógica a Utilidad

Crear resources/js/utils/twMerge.js:

javascript

import { twMerge as merge } from 'tailwind-merge'


export const twMerge = (baseClasses, ...additionalClasses) => {

    return merge(baseClasses, ...additionalClasses)

}


// Función para crear variantes de botones

export const buttonVariants = {

    primary: 'bg-blue-600 hover:bg-blue-700 text-white',

    secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800',

    danger: 'bg-red-600 hover:bg-red-700 text-white',

    success: 'bg-green-600 hover:bg-green-700 text-white',

}

6.2. Componente Genérico de Botón

vue

<script setup>

import { computed } from 'vue'

import { Link } from '@inertiajs/vue3'

import { twMerge, buttonVariants } from '@/utils/twMerge'


const props = defineProps({

    variant: {

        type: String,

        default: 'primary',

        validator: (value) => ['primary', 'secondary', 'danger', 'success'].includes(value)

    },

    href: {

        type: String,

        default: ''

    },

    class: {

        type: String,

        default: ''

    },

    ariaLabel: {

        type: String,

        required: true

    }

})


const baseClasses = computed(() => {

    return twMerge(

        'px-4 py-2 rounded-md font-semibold transition duration-150 ease-in-out',

        buttonVariants[props.variant],

        props.class

    )

})

</script>


<template>

    <Link v-if="href" :href="href" :class="baseClasses" :aria-label="ariaLabel">

        <slot />

    </Link>

    <button v-else :class="baseClasses" :aria-label="ariaLabel">

        <slot />

    </button>

</template>

6.3. Uso con Variantes

vue

<!-- Botón primario (default) -->

<Button aria-label="Guardar">Guardar</Button>


<!-- Botón secundario -->

<Button variant="secondary" aria-label="Cancelar">Cancelar</Button>


<!-- Botón de peligro -->

<Button variant="danger" aria-label="Eliminar">Eliminar</Button>


<!-- Botón de éxito -->

<Button variant="success" aria-label="Aceptar">Aceptar</Button>


<!-- Con href -->

<Button href="/chatbots" variant="primary" aria-label="Ver chatbots">

    Ver Chatbots

</Button>


7. Ejemplos Prácticos

7.1. Componente ChatbotListItem Actualizado

vue

<script setup>

import dayjs, { timeAgo } from '@/utils/dayjs'

import { PrimaryButton, SecondaryButton, DangerButton } from '@/Components/Buttons'


defineProps({

    chatbot: {

        type: Object,

        required: true

    }

})

</script>


<template>

    <div class="flex flex-col md:flex-row md:items-center md:justify-between py-4">

        <div class="flex-1">

            <div class="text-gray-800 dark:text-gray-200 font-medium">

                {{ chatbot.name }}

            </div>

            <div class="text-sm text-gray-500 dark:text-gray-400">

                Creado: {{ timeAgo(chatbot.created_at) }}

            </div>

        </div>


        <div class="flex space-x-2 mt-4 md:mt-0">

            <!-- Ahora los botones pueden ser links -->

            <PrimaryButton 

                :href="route('chatbots.show', chatbot.id)"

                aria-label="Ver chatbot"

            >

                Ver

            </PrimaryButton>

            

            <SecondaryButton 

                :href="route('chatbots.edit', chatbot.id)"

                aria-label="Editar chatbot"

            >

                Editar

            </SecondaryButton>

            

            <DangerButton 

                aria-label="Eliminar chatbot"

                @click="$emit('delete', chatbot.id)"

            >

                Eliminar

            </DangerButton>

        </div>

    </div>

</template>

7.2. Botón con Icono

vue

<script setup>

import { twMerge } from '@/utils/twMerge'


const props = defineProps({

    icon: {

        type: Boolean,

        default: false

    },

    // ... otros props

})


const classes = computed(() => {

    return twMerge(

        'inline-flex items-center gap-2',

        props.icon && 'px-2 py-2',

        !props.icon && 'px-4 py-2',

        defaultClasses,

        props.class

    )

})

</script>


<template>

    <button :class="classes">

        <slot name="icon" />

        <slot v-if="!icon" />

    </button>

</template>


<!-- Uso -->

<Button icon aria-label="Ajustes">

    <template #icon>

        <svg class="w-5 h-5">...</svg>

    </template>

</Button>


8. Configuración Avanzada

8.1. Configuración Personalizada

javascript

import { extendTailwindMerge } from 'tailwind-merge'


const customTwMerge = extendTailwindMerge({

    extend: {

        classGroups: {

            'my-custom-class': ['custom-class-1', 'custom-class-2'],

        },

        conflictingClassGroups: {

            'my-custom-class': ['another-class'],

        },

    }

})

8.2. Plugins de Tailwind

javascript

// Con plugins de Tailwind

import { twMerge } from 'tailwind-merge'

import { withTailwind } from 'tailwind-merge/with-tailwind'


const merge = withTailwind({

    // Configuración con plugins

})


// O usar directamente

const merged = twMerge('px-2 py-1', 'px-4')


9. Buenas Prácticas

9.1. Organización de Clases

javascript

// ❌ Mal: Desorganizado

const classes = twMerge(

    'p-4',

    'text-center',

    'bg-white',

    'shadow-lg',

    'rounded',

    props.class

)


// ✅ Bien: Agrupado lógicamente

const classes = computed(() => {

    return twMerge(

        // Layout

        'flex items-center justify-between',

        // Espaciado

        'p-4 space-x-4',

        // Estilos

        'bg-white shadow-lg rounded-lg',

        // Estados

        'hover:shadow-xl transition-shadow duration-300',

        // Clases personalizadas

        props.class

    )

})

9.2. Reutilización de Estilos

javascript

// utils/tailwind.js

export const baseButtonClasses = `

    inline-flex

    items-center

    justify-center

    px-4

    py-2

    rounded-md

    font-medium

    transition-colors

    duration-200

`


export const buttonSizes = {

    sm: 'px-3 py-1.5 text-sm',

    md: 'px-4 py-2 text-base',

    lg: 'px-6 py-3 text-lg'

}


10. Resumen

10.1. Beneficios de Tailwind Merge

  • ✅ Resuelve conflictos automáticamente

  • ✅ Mejora la reutilización de componentes

  • ✅ Flexibilidad para sobrescribir estilos

  • ✅ Mantiene la consistencia visual

10.2. Archivos Modificados

Archivo

Cambio

PrimaryButton.vue

Agregar soporte para href y twMerge

SecondaryButton.vue

Actualizar con twMerge

DangerButton.vue

Actualizar con twMerge

10.3. Instalación

bash

npm install --save-dev tailwind-merge

¡Ahora los botones pueden ser links y mantener estilos consistentes! 


Comentarios

Entradas más populares de este blog

1. Introducción al curso de Chatbots personalizados con Laravel & OpenAI

2. Prerequisitos para el Curso de Chatbots con Laravel y OpenAI

4. Herramientas de Calidad