Repository: tailwindlabs/tailwindcss (TypeScript)

Issue:
Preserve HSL colors instead of converting to RGBA (keeps colors on older browsers)
### What version of Tailwind CSS are you using?

v2.0.4

### What version of Node.js are you using?

v14.16.0

### What browser are you using?

N/A

### What operating system are you using?

macOS

### Reproduction repository

see body

I'm using HSL color definitions in a project instead of RGB/hex. With Tailwind 2.0, the `hsl()` values in my tailwind.config.js get converted to `rgba()` rules (so the opacity classes can [work their dark magic](https://adamwathan.me/composing-the-uncomposable-with-css-variables/)). However, because HSL -> RGB isn't a simple add/subtract swap, the resulting CSS uses floats for each color channel:
```js
// tailwind.config.js
module.exports = {
  theme: {
    colors: {
      offblack: 'hsl(205,95%,2%)'
} } }

```
```css
/* resulting CSS */
.text-offblack {
    --tw-text-opacity: 1;
    color: rgba(.2550000000000002,5.907499999999999,9.945,var(--tw-text-opacity));
}
```
The trouble with this is that [browser support for floats in rgba](https://caniuse.com/mdn-css_types_color_floats_in_rgb_rgba) wasn't good until the last couple years. That means people with older browsers (something like 5% of traffic) **will see NO COLORS AT ALL** on my site with Tailwind 2.0 — everything is black and white.

Can we make Tailwind keep HSL values and output `hsla()` colors instead of `rgba()`?

_Originally posted by @andronocean in https://github.com/tailwindlabs/tailwindcss/discussions/3833_


Hints:
I don't really see how this would matter considering the stats of people using old browsers are a very small minority
![image](https://nbb.rocks/i/Fl4dB.png)
Safari is actually the biggest problem for browser support, vis a vis current usage. It didn't support floats until late 2018/early 2019 (somewhere in v12.1.x) And since iOS safari versions are pegged to iOS upgrades, a lot of older iPhones and iPads cannot get support now. So depending on the geography and device trends of your visitors, you could be saying 3% - 10% of visitors get no colors ([caniuse data](https://caniuse.com/mdn-css_types_color_floats_in_rgb_rgba)):

<img width="655" alt="Screen Shot 2021-03-24 at 8 01 33 PM" src="https://user-images.githubusercontent.com/26490481/112399011-ce315c80-8cdb-11eb-9f80-204adea55ab4.png">

Personally, I think colors are pretty dang fundamental to the web, and need to work everywhere they reasonably can.

But furthermore, I think the developer experience is important. If I define HSL colors, I'm doing so for a reason, and there's no good reason for Tailwind to change them.

Admittedly, a good chunk of the browsers that this breaks on also lack custom properties support, so the colors still wouldn't work out of the box for them, due to the opacity variable. Tailwind is upfront about its use of custom properties, however, so if that matters to a developer they are likely to address it with some sort of fallback. It's not going to be an undocumented surprise like the HSL > RGBA behavior in question.
Just note that `hsl()` could also be converted to `hsla()`.
I can raise a PR if needed.

----
edit: @andronocean have you tried to use `hsla()` instead of `hsl()? Because it should not transform it.
@fedeci it's indeed not transforming it to `rgba` if you use `hsla`, but the problem is you lose the capacity to apply opacity utilities like `text-opacity-{n}` or `bg-opacity-{n}`.

It basically returns the `hsla` color only, instead of composing it with a CSS variable for the alpha transparency.

I've actually just recorded a video that covers how to "bring back" the support for opacity in colors, by defining colors as functions. It doesn't cover `hsla` in particular, but this would be a possible solution -  we'll release that video soon.

You can see the code responsible for the "transform" here:

https://github.com/tailwindlabs/tailwindcss/blob/master/src/util/withAlphaVariable.js

But like Adam mentioned in the GitHub discussion, we could probably implement support for `hsla` colors, so users don't have to manually compose them with Tailwind's internal `opacityVariable` that is used to set the alpha transparency channel 👍

Patch:
diff --git a/src/plugins/gradientColorStops.js b/src/plugins/gradientColorStops.js
--- a/src/plugins/gradientColorStops.js
+++ b/src/plugins/gradientColorStops.js
@@ -2,7 +2,7 @@ import _ from 'lodash'
 import flattenColorPalette from '../util/flattenColorPalette'
 import nameClass from '../util/nameClass'
 import toColorValue from '../util/toColorValue'
-import { toRgba } from '../util/withAlphaVariable'
+import { toRgba, toHsla } from '../util/withAlphaVariable'
 
 export default function () {
   return function ({ addUtilities, theme, variants }) {
@@ -16,8 +16,9 @@ export default function () {
           }
 
           try {
-            const [r, g, b] = toRgba(value)
-            return `rgba(${r}, ${g}, ${b}, 0)`
+            const isHSL = value.startsWith('hsl')
+            const [i, j, k] = isHSL ? toHsla(value) : toRgba(value)
+            return `${isHSL ? 'hsla' : 'rgba'}(${i}, ${j}, ${k}, 0)`
           } catch (_error) {
             return `rgba(255, 255, 255, 0)`
           }
diff --git a/src/plugins/ringWidth.js b/src/plugins/ringWidth.js
--- a/src/plugins/ringWidth.js
+++ b/src/plugins/ringWidth.js
@@ -1,20 +1,21 @@
 import _ from 'lodash'
 import nameClass from '../util/nameClass'
-import { toRgba } from '../util/withAlphaVariable'
+import { toHsla, toRgba } from '../util/withAlphaVariable'
 
 export default function () {
   return function ({ addUtilities, theme, variants }) {
-    function safeCall(callback, defaultValue) {
+    const ringColorDefault = (() => {
+      const isHSL = (theme('ringColor.DEFAULT') || '').startsWith('hsl')
+      const opacity = theme('ringOpacity.DEFAULT', '0.5')
       try {
-        return callback()
+        const [i, j, k] = isHSL
+          ? toHsla(theme('ringColor.DEFAULT'))
+          : toRgba(theme('ringColor.DEFAULT'))
+        return `${isHSL ? 'hsla' : 'rgba'}(${i}, ${j}, ${k}, ${opacity})`
       } catch (_error) {
-        return defaultValue
+        return `rgba(147, 197, 253, ${opacity})`
       }
-    }
-
-    const ringColorDefault = (([r, g, b]) => {
-      return `rgba(${r}, ${g}, ${b}, ${theme('ringOpacity.DEFAULT', '0.5')})`
-    })(safeCall(() => toRgba(theme('ringColor.DEFAULT')), ['147', '197', '253']))
+    })()
 
     addUtilities(
       {
diff --git a/src/util/withAlphaVariable.js b/src/util/withAlphaVariable.js
--- a/src/util/withAlphaVariable.js
+++ b/src/util/withAlphaVariable.js
@@ -16,6 +16,12 @@ export function toRgba(color) {
   return [r, g, b, a === undefined && hasAlpha(color) ? 1 : a]
 }
 
+export function toHsla(color) {
+  const [h, s, l, a] = createColor(color).hsl().array()
+
+  return [h, `${s}%`, `${l}%`, a === undefined && hasAlpha(color) ? 1 : a]
+}
+
 export default function withAlphaVariable({ color, property, variable }) {
   if (_.isFunction(color)) {
     return {
@@ -25,7 +31,9 @@ export default function withAlphaVariable({ color, property, variable }) {
   }
 
   try {
-    const [r, g, b, a] = toRgba(color)
+    const isHSL = color.startsWith('hsl')
+
+    const [i, j, k, a] = isHSL ? toHsla(color) : toRgba(color)
 
     if (a !== undefined) {
       return {
@@ -35,7 +43,7 @@ export default function withAlphaVariable({ color, property, variable }) {
 
     return {
       [variable]: '1',
-      [property]: `rgba(${r}, ${g}, ${b}, var(${variable}))`,
+      [property]: `${isHSL ? 'hsla' : 'rgba'}(${i}, ${j}, ${k}, var(${variable}))`,
     }
   } catch (error) {
     return {


Test patch:
diff --git a/__tests__/plugins/gradientColorStops.test.js b/__tests__/plugins/gradientColorStops.test.js
--- a/__tests__/plugins/gradientColorStops.test.js
+++ b/__tests__/plugins/gradientColorStops.test.js
@@ -17,6 +17,7 @@ test('opacity variables are given to colors defined as closures', () => {
 
             return `rgb(31,31,31)`
           },
+          secondary: 'hsl(10, 50%, 50%)',
         },
         opacity: {
           50: '0.5',
@@ -33,23 +34,47 @@ test('opacity variables are given to colors defined as closures', () => {
     .process('@tailwind utilities', { from: undefined })
     .then((result) => {
       const expected = `
-        .text-primary {
-          --tw-text-opacity: 1;
-          color: rgba(31,31,31,var(--tw-text-opacity))
-        }
-        .text-opacity-50 {
-          --tw-text-opacity: 0.5
-        }
-				.from-primary {
-					--tw-gradient-from: rgb(31,31,31);
-					--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(31, 31, 31, 0))
-				}
-				.via-primary {
-					--tw-gradient-stops: var(--tw-gradient-from), rgb(31,31,31), var(--tw-gradient-to, rgba(31, 31, 31, 0))
-				}
-				.to-primary {
-					--tw-gradient-to: rgb(31,31,31)
-				}
+      .text-primary {
+        --tw-text-opacity: 1;
+        color: rgba(31, 31, 31, var(--tw-text-opacity));
+      }
+
+      .text-secondary {
+        --tw-text-opacity: 1;
+        color: hsla(10, 50%, 50%, var(--tw-text-opacity));
+      }
+
+      .text-opacity-50 {
+        --tw-text-opacity: 0.5;
+      }
+
+      .from-primary {
+        --tw-gradient-from: rgb(31, 31, 31);
+        --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(31, 31, 31, 0));
+      }
+
+      .from-secondary {
+        --tw-gradient-from: hsl(10, 50%, 50%);
+        --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, hsla(10, 50%, 50%, 0));
+      }
+
+      .via-primary {
+        --tw-gradient-stops: var(--tw-gradient-from), rgb(31, 31, 31),
+        var(--tw-gradient-to, rgba(31, 31, 31, 0));
+      }
+
+      .via-secondary {
+        --tw-gradient-stops: var(--tw-gradient-from), hsl(10, 50%, 50%),
+        var(--tw-gradient-to, hsla(10, 50%, 50%, 0));
+      }
+
+      .to-primary {
+        --tw-gradient-to: rgb(31, 31, 31);
+      }
+
+      .to-secondary {
+       --tw-gradient-to: hsl(10, 50%, 50%);
+      }
       `
 
       expect(result.css).toMatchCss(expected)
diff --git a/__tests__/plugins/ringWidth.test.js b/__tests__/plugins/ringWidth.test.js
--- a/__tests__/plugins/ringWidth.test.js
+++ b/__tests__/plugins/ringWidth.test.js
@@ -61,6 +61,51 @@ test('ring widths', () => {
   ])
 })
 
+test('ring widths with defaults and hsl value for ringColor', () => {
+  const config = {
+    theme: {
+      ringWidth: {},
+      ringOffsetWidth: {
+        DEFAULT: '2px',
+      },
+      ringOffsetColor: {
+        DEFAULT: 'pink',
+      },
+      ringColor: {
+        DEFAULT: 'hsl(10, 50%, 50%)',
+      },
+    },
+    variants: {
+      ringColor: [],
+    },
+  }
+
+  const { utilities } = invokePlugin(plugin(), config)
+  expect(utilities).toEqual([
+    [
+      {
+        '*': {
+          '--tw-ring-color': 'hsla(10, 50%, 50%, 0.5)',
+          '--tw-ring-inset': 'var(--tw-empty,/*!*/ /*!*/)',
+          '--tw-ring-offset-color': 'pink',
+          '--tw-ring-offset-shadow': '0 0 #0000',
+          '--tw-ring-offset-width': '2px',
+          '--tw-ring-shadow': '0 0 #0000',
+        },
+      },
+      { respectImportant: false },
+    ],
+    [
+      {
+        '.ring-inset': {
+          '--tw-ring-inset': 'inset',
+        },
+      },
+      undefined,
+    ],
+  ])
+})
+
 test('ring widths with defaults', () => {
   const config = {
     theme: {
diff --git a/__tests__/withAlphaVariable.test.js b/__tests__/withAlphaVariable.test.js
--- a/__tests__/withAlphaVariable.test.js
+++ b/__tests__/withAlphaVariable.test.js
@@ -118,3 +118,26 @@ test('it allows a closure to be passed', () => {
     'background-color': 'rgba(0, 0, 0, var(--tw-bg-opacity))',
   })
 })
+
+test('it transforms rgb and hsl to rgba and hsla', () => {
+  expect(
+    withAlphaVariable({
+      color: 'rgb(50, 50, 50)',
+      property: 'background-color',
+      variable: '--tw-bg-opacity',
+    })
+  ).toEqual({
+    '--tw-bg-opacity': '1',
+    'background-color': 'rgba(50, 50, 50, var(--tw-bg-opacity))',
+  })
+  expect(
+    withAlphaVariable({
+      color: 'hsl(50, 50%, 50%)',
+      property: 'background-color',
+      variable: '--tw-bg-opacity',
+    })
+  ).toEqual({
+    '--tw-bg-opacity': '1',
+    'background-color': 'hsla(50, 50%, 50%, var(--tw-bg-opacity))',
+  })
+})