Skip to content

[Attempt] Exploring the Coexistence of Nuxt UI v3 and UnoCSS #3011

@byronogis

Description

@byronogis

Related #196

Alias Name Version
ui nuxt/ui v3.0.0-alpha.10
tw tailwindcss v4.0.0-beta.8
tv tailwind-variants v0.3.0
tm tailwind-merge v2.6.0
uno unocss v0.65.3

ui internally uses tv to flexibly generate component class names. tv internally uses tm to merge classes with the same effect (e.g., p-2 p-4 --> p-4), and finally uses tw to recognize class names and generate style classes.

After examining tv and tm, it became apparent that they don't strongly depend on tw, which opens up the possibility of replacing the final style generation step with uno.

## tw Prefix Attempt (Optional)

Initially, the idea was to limit tw to ui internals and uno for user usage. With tw's prefix feature, the class rules became simpler. However, ui internally (v3.0.0-alpha.10) doesn't yet support passing tw prefix configuration, so let's first make ui support tw prefixes.

Understanding tv Variants

To add prefixes, we need to analyze the data structure that tv accepts.

Regular Variants

  import { tv } from 'tailwind-variants';

  tv({
    variants: {
      foo: {
        foo_val: 'px-2', // or ['pxx-2', ...], be the same, class / className
      },
    }
  })({ foo: 'foo_val' })

Boolean Variants (Including true/false keys)

  tv({
    variants: {
      foo: {
        true: 'px-2',
        false: 'py-2',
      }
    }
  })({ foo: true, })

Compound Variants (Intersection between variants)

  tv({
    variants: {
      foo: {
        foo_val: 'px-2',
        foo_val2: 'px-2',
      },
      bar: {
        true: 'px-2',
        false: 'py-2',
      },
    },
    compoundVariants: [
      {
        foo: 'foo_val', // or ['foo_val', 'foo_val2', ]
        bar: true,
        class: 'text-white', // You can also use "className" as the key
      }
    ],
  })({ foo: true, })

Responsive Variants (Device screen sizes)

  tv({
    variants: {
      foo: {
        foo_val: 'px-2',
        foo_val2: 'px-4',
      },
    }
  },
  {
    responsiveVariants: ['sm', 'md'] // or `true` for all
  })({ 
    foo: {
      initial: 'foo_val',
      sm: 'foo_val',
      md: 'foo_val2',
    }
  })

Slot Variants (Combining above variants with slots)

  tv({
    slots: {
      slot_a: 'mx-2',
      slot_b: 'mx-2',
    },
    compoundSlots: [
      {
        slots: ['slot_a', 'slot_b', ], 
        class: '',
        foo: 'foo_val', // for all slot while variant is missing
      },
    ],
    variants: {
      foo: {
        foo_val: {
          slot_a: 'px-2',
        }
      },
      bar: {
        true: {
          slot_a: 'mx-2'
        },
        false: {
          slot_a: 'mx-0'
        },
      },
    },
    compoundVariants: [
      {
        foo: 'foo_val',
        bar: true,
        class: {
          slot_a: '',
        }
      }
    ],
    {
      responsiveVariants: ['sm', 'md'] // or `true` for all
    }
  })({ 
    foo: {
      initial: 'foo_val',
      sm: 'foo_val',
      md: 'foo_val2',
    }
  }) // --> { foo_slot, bar_slot, }

Summary

After adding prefix functionality locally (#3009 ) and configuring tw prefix (while installing uno to handle non-prefix classes in the playground for page styling), practical usage revealed some unavoidable conflicts, such as at-rule usage. Additionally, this approach doesn't effectively prevent files from being processed twice (by both tw & uno). Therefore, let's try a different approach: remove tw completely and use UnoCSS for everything!

Modifying ui

Disabling tw Plugin

Remove @tailwindcss/vite installation handling from ui internals.
Remove tw imports and specific configurations from ui playground.

Introducing uno

For nuxt | vite

Also, Style Reset can be applied as needed.

IMPORTANT: Include @nuxt/ui files

export default defineConfig({
  presets: [
    presetUno(),
  ],
  content: {
    pipeline: {
      include: [
        // the default
        /\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/,
        // IMPORTANT include @nuxt/ui files
        /\.nuxt\/ui\//,
      ]
    }
  }
})

Primitive Variables

uno doesn't generate primitive variables like tw does (e.g., --color-green-500, --text-lg, --radius-md, etc.), but ui internally needs these variables.

For now, we'll directly copy the primitive variables from tw's normal loaded to local.

Rule Inconsistencies

There are must more undiscovered issues

Ring

In uno, the default ring width is 3px, different from tw's 1px.

Solution:

  shortcuts: {
    ring: 'ring-1'
  },

Color Opacity

tw internally uses color-mix, allowing direct opacity settings for color variables.
Currently, uno doesn't support this directly.

tw: class="bg-[var(--color-primary)]/20"
uno: class="bg-[var(--color-primary)]/20"
uno: class="bg-[rgb(255,5,5)]/20"

:root {
  --color-primary: rgb(255, 5, 5);
}

After observing ui usage, uno rules were added:

uno: class="bg-[var(--ui-primaey)]/20"
uno: class="hover:bg-[var(--ui-primaey)]/75"

Solution:

  rules: [
    [/(?:([^:\s]+):)?bg-.*?\[(var\(--[^-]+-[^)]+\))\]\/(\d+)/, function* ([, modifier, color, alpha], { symbols }) {
      yield {
        background: `color-mix(in oklab, ${color} ${alpha}%, transparent)`
      }
      if (modifier) {
        yield {
          [symbols.selector]: selector => `${selector}:${modifier}`,
          background: `color-mix(in oklab, ${color} ${alpha}%, transparent)`
        }
      }
    }],
    [/(?:([^:\s]+):)?text-.*?\[(var\(--[^-]+-[^)]+\))\]\/(\d+)/, function* ([, modifier, color, alpha], { symbols }) {
      yield {
        color: `color-mix(in oklab, ${color} ${alpha}%, transparent)`
      }
      if (modifier) {
        yield {
          [symbols.selector]: selector => `${selector}:${modifier}`,
          color: `color-mix(in oklab, ${color} ${alpha}%, transparent)`
        }
      }
    }]
  ],

Now, let's run the playground! ~

Notes

tm's handling of tw and uno exclusive rules yields unexpected results

tw doesn't directly support units (px, em, ...) like text-Xpx, requiring text-[Xpx] instead.

  • text-md text-[20px] --> text-[20px]
  • text-md text-20px --> text-md text-20px
  • text-16px text-20px --> text-20px
  • text-[16px] text-[20px] --> text-[20px]

When using ui, it's recommended to pass classes that tw can also parse.

Commits · byronogis/ui


Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions