Repository: tailwindlabs/tailwindcss (ts)

Issue:
[JIT] font/text size does not support css variables
### What version of Tailwind CSS are you using?

2.1.0

### What build tool (or framework if it abstracts the build tool) are you using?

postcss8

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

15.14.0

### What browser are you using?

Firefox

### What operating system are you using?

Arch Linux

### Reproduction repository

See comment

### Describe your issue

Hey guys!
I try to implement fluid font sizes according to https://www.smashingmagazine.com/2021/04/designing-developing-fluid-type-space-scales/

I have [generated the following pattern](https://utopia.fyi/type/calculator):
```css
/* @link https://utopia.fyi/type/calculator?c=320,21,1.2,1140,24,1.25,5,2,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l */

:root {
  --step--2: clamp(0.91rem, 0.89rem + 0.10vw, 0.96rem);
  --step--1: clamp(1.09rem, 1.05rem + 0.21vw, 1.20rem);
  --step-0: clamp(1.31rem, 1.24rem + 0.37vw, 1.50rem);
  --step-1: clamp(1.58rem, 1.46rem + 0.59vw, 1.88rem);
  --step-2: clamp(1.89rem, 1.71rem + 0.89vw, 2.34rem);
  --step-3: clamp(2.27rem, 2.01rem + 1.29vw, 2.93rem);
  --step-4: clamp(2.72rem, 2.36rem + 1.83vw, 3.66rem);
  --step-5: clamp(3.27rem, 2.75rem + 2.56vw, 4.58rem);
}
```

But the syntax `text-[var(--step-5)]` does not work. Using a fixed pixel size works, and using the style inline also works.

```html
<div class="text-[var(--step-5)]">test1</div>

<div class="text-[50px]">test2</div>

<div style="font-size: var(--step-5);">test3</div>
```

Pull request description:
Support forcing coercion type with arbitrary value syntax
Resolves #4096.

This PR makes it possible to force the type of a coerced arbitrary value in order to get around collisions between utilities when using arbitrary values with JIT.

For example, in this example, it's impossible to know if this is a font-size utility or a color utility:

```html
<div class="text-[var(--mystery)]">
```

To make it possible for this to work, you can now prefix the arbitrary value with `{type}:` to provide a hint to the compiler about what the resulting value is going to be so it is passed to the correct plugin:

```html
<div class="text-[color:var(--mystery)] text-[length:var(--surprise)]">
```

The valid values match the types supported by `matchUtilities`:

- `length`
- `color`
- `angle`
- `list`

...which generally correspond to the official CSS data type names. We will probably keep refining this but I think it's a good start. May also alias `length` to `size` since `length` is unintuitive for things like font-size, although it is the correct term.

Patch:
diff --git a/src/jit/lib/expandTailwindAtRules.js b/src/jit/lib/expandTailwindAtRules.js
index 1560895b..cd528f1c 100644
--- a/src/jit/lib/expandTailwindAtRules.js
+++ b/src/jit/lib/expandTailwindAtRules.js
@@ -25,9 +25,12 @@ function getDefaultExtractor(fileExtension) {
   }
 }
 
-function getExtractor(fileName, tailwindConfig) {
+function getExtractor(tailwindConfig, fileExtension) {
   const purgeOptions = tailwindConfig && tailwindConfig.purge && tailwindConfig.purge.options
-  const fileExtension = path.extname(fileName).slice(1)
+
+  if (!fileExtension) {
+    return (purgeOptions && purgeOptions.defaultExtractor) || getDefaultExtractor()
+  }
 
   if (!purgeOptions) {
     return getDefaultExtractor(fileExtension)
@@ -208,11 +211,16 @@ export default function expandTailwindAtRules(context, registerDependency) {
     env.DEBUG && console.time('Reading changed files')
     for (let file of context.changedFiles) {
       let content = fs.readFileSync(file, 'utf8')
-      let extractor = getExtractor(file, context.tailwindConfig)
+      let extractor = getExtractor(context.tailwindConfig, path.extname(file).slice(1))
       getClassCandidates(content, extractor, contentMatchCache, candidates, seen)
     }
     env.DEBUG && console.timeEnd('Reading changed files')
 
+    for (let { content, extension } of context.rawContent) {
+      let extractor = getExtractor(context.tailwindConfig, extension)
+      getClassCandidates(content, extractor, contentMatchCache, candidates, seen)
+    }
+
     // ---
 
     // Generate the actual CSS
diff --git a/src/jit/lib/generateRules.js b/src/jit/lib/generateRules.js
index eaae34d8..027ad707 100644
--- a/src/jit/lib/generateRules.js
+++ b/src/jit/lib/generateRules.js
@@ -192,9 +192,13 @@ function* resolveMatchedPlugins(classCandidate, context) {
   }
 }
 
+function splitWithSeparator(input, separator) {
+  return input.split(new RegExp(`\\${separator}(?![^[]*\\])`, 'g'))
+}
+
 function* resolveMatches(candidate, context) {
   let separator = context.tailwindConfig.separator
-  let [classCandidate, ...variants] = candidate.split(separator).reverse()
+  let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse()
   let important = false
 
   if (classCandidate.startsWith('!')) {
diff --git a/src/jit/lib/setupContext.js b/src/jit/lib/setupContext.js
index 844b7ede..d664f667 100644
--- a/src/jit/lib/setupContext.js
+++ b/src/jit/lib/setupContext.js
@@ -534,9 +534,9 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
 
         function wrapped(modifier) {
           let { type = 'any' } = options
-          let value = coerceValue(type, modifier, options.values)
+          let [value, coercedType] = coerceValue(type, modifier, options.values)
 
-          if (value === undefined) {
+          if (type !== coercedType || value === undefined) {
             return []
           }
 
@@ -767,6 +767,10 @@ export default function setupContext(configOrPath) {
 
     process.env.DEBUG && console.log('Setting up new context...')
 
+    let purgeContent = Array.isArray(tailwindConfig.purge)
+      ? tailwindConfig.purge
+      : tailwindConfig.purge.content
+
     let context = {
       changedFiles: new Set(),
       ruleCache: new Set(),
@@ -781,10 +785,12 @@ export default function setupContext(configOrPath) {
       configPath: userConfigPath,
       tailwindConfig: tailwindConfig,
       configDependencies: new Set(),
-      candidateFiles: (Array.isArray(tailwindConfig.purge)
-        ? tailwindConfig.purge
-        : tailwindConfig.purge.content
-      ).map((path) => normalizePath(path)),
+      candidateFiles: purgeContent
+        .filter((item) => typeof item === 'string')
+        .map((path) => normalizePath(path)),
+      rawContent: purgeContent
+        .filter((item) => typeof item.raw === 'string')
+        .map(({ raw, extension }) => ({ content: raw, extension })),
       variantMap: new Map(),
       stylesheetCache: null,
       fileModifiedMap: new Map(),
diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js
index 3b246b17..acc9e4a9 100644
--- a/src/util/pluginUtils.js
+++ b/src/util/pluginUtils.js
@@ -204,6 +204,18 @@ let typeMap = {
   lookup: asLookupValue,
 }
 
+function splitAtFirst(input, delim) {
+  return (([first, ...rest]) => [first, rest.join(delim)])(input.split(delim))
+}
+
 export function coerceValue(type, modifier, values) {
-  return typeMap[type](modifier, values)
+  if (modifier.startsWith('[') && modifier.endsWith(']')) {
+    let [explicitType, value] = splitAtFirst(modifier.slice(1, -1), ':')
+
+    if (value.length > 0 && Object.keys(typeMap).includes(explicitType)) {
+      return [asValue(`[${value}]`, values), explicitType]
+    }
+  }
+
+  return [typeMap[type](modifier, values), type]
 }


Test patch:
diff --git a/tests/jit/arbitrary-values.test.css b/tests/jit/arbitrary-values.test.css
index cec88fb5..17919c8a 100644
--- a/tests/jit/arbitrary-values.test.css
+++ b/tests/jit/arbitrary-values.test.css
@@ -269,12 +269,18 @@
 .text-\[2\.23rem\] {
   font-size: 2.23rem;
 }
+.text-\[length\:var\(--font-size\)\] {
+  font-size: var(--font-size);
+}
 .leading-\[var\(--leading\)\] {
   line-height: var(--leading);
 }
 .tracking-\[var\(--tracking\)\] {
   letter-spacing: var(--tracking);
 }
+.text-\[color\:var\(--color\)\] {
+  color: var(--color);
+}
 .placeholder-\[var\(--placeholder\)\]::placeholder {
   color: var(--placeholder);
 }
diff --git a/tests/jit/arbitrary-values.test.html b/tests/jit/arbitrary-values.test.html
index 0f89ce24..13b2b98b 100644
--- a/tests/jit/arbitrary-values.test.html
+++ b/tests/jit/arbitrary-values.test.html
@@ -54,6 +54,9 @@
     <div class="skew-x-[3px]"></div>
     <div class="skew-y-[3px]"></div>
     <div class="text-[2.23rem]"></div>
+    <div class="text-[length:var(--font-size)]"></div>
+    <div class="text-[color:var(--color)]"></div>
+    <div class="text-[angle:var(--angle)]"></div>
     <div class="duration-[2s]"></div>
     <div class="m-[7px]"></div>
     <div class="mx-[7px]"></div>
diff --git a/tests/jit/raw-content.test.css b/tests/jit/raw-content.test.css
new file mode 100644
index 00000000..aec70a93
--- /dev/null
+++ b/tests/jit/raw-content.test.css
@@ -0,0 +1,757 @@
+* {
+  --tw-shadow: 0 0 #0000;
+  --tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
+  --tw-ring-offset-width: 0px;
+  --tw-ring-offset-color: #fff;
+  --tw-ring-color: rgba(59, 130, 246, 0.5);
+  --tw-ring-offset-shadow: 0 0 #0000;
+  --tw-ring-shadow: 0 0 #0000;
+}
+.container {
+  width: 100%;
+}
+@media (min-width: 640px) {
+  .container {
+    max-width: 640px;
+  }
+}
+@media (min-width: 768px) {
+  .container {
+    max-width: 768px;
+  }
+}
+@media (min-width: 1024px) {
+  .container {
+    max-width: 1024px;
+  }
+}
+@media (min-width: 1280px) {
+  .container {
+    max-width: 1280px;
+  }
+}
+@media (min-width: 1536px) {
+  .container {
+    max-width: 1536px;
+  }
+}
+.sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  white-space: nowrap;
+  border-width: 0;
+}
+.pointer-events-none {
+  pointer-events: none;
+}
+.invisible {
+  visibility: hidden;
+}
+.absolute {
+  position: absolute;
+}
+.inset-0 {
+  top: 0px;
+  right: 0px;
+  bottom: 0px;
+  left: 0px;
+}
+.inset-y-4 {
+  top: 1rem;
+  bottom: 1rem;
+}
+.inset-x-2 {
+  left: 0.5rem;
+  right: 0.5rem;
+}
+.top-6 {
+  top: 1.5rem;
+}
+.right-8 {
+  right: 2rem;
+}
+.bottom-12 {
+  bottom: 3rem;
+}
+.left-16 {
+  left: 4rem;
+}
+.isolate {
+  isolation: isolate;
+}
+.isolation-auto {
+  isolation: auto;
+}
+.z-30 {
+  z-index: 30;
+}
+.order-last {
+  order: 9999;
+}
+.order-2 {
+  order: 2;
+}
+.col-span-3 {
+  grid-column: span 3 / span 3;
+}
+.col-start-1 {
+  grid-column-start: 1;
+}
+.col-end-4 {
+  grid-column-end: 4;
+}
+.row-span-2 {
+  grid-row: span 2 / span 2;
+}
+.row-start-3 {
+  grid-row-start: 3;
+}
+.row-end-5 {
+  grid-row-end: 5;
+}
+.float-right {
+  float: right;
+}
+.clear-left {
+  clear: left;
+}
+.m-4 {
+  margin: 1rem;
+}
+.my-2 {
+  margin-top: 0.5rem;
+  margin-bottom: 0.5rem;
+}
+.mx-auto {
+  margin-left: auto;
+  margin-right: auto;
+}
+.mt-0 {
+  margin-top: 0px;
+}
+.mr-1 {
+  margin-right: 0.25rem;
+}
+.mb-3 {
+  margin-bottom: 0.75rem;
+}
+.ml-4 {
+  margin-left: 1rem;
+}
+.box-border {
+  box-sizing: border-box;
+}
+.inline-grid {
+  display: inline-grid;
+}
+.hidden {
+  display: none;
+}
+.h-16 {
+  height: 4rem;
+}
+.max-h-screen {
+  max-height: 100vh;
+}
+.min-h-0 {
+  min-height: 0px;
+}
+.w-12 {
+  width: 3rem;
+}
+.min-w-min {
+  min-width: min-content;
+}
+.max-w-full {
+  max-width: 100%;
+}
+.flex-1 {
+  flex: 1 1 0%;
+}
+.flex-shrink {
+  flex-shrink: 1;
+}
+.flex-shrink-0 {
+  flex-shrink: 0;
+}
+.flex-grow {
+  flex-grow: 1;
+}
+.flex-grow-0 {
+  flex-grow: 0;
+}
+.table-fixed {
+  table-layout: fixed;
+}
+.border-collapse {
+  border-collapse: collapse;
+}
+.transform {
+  --tw-translate-x: 0;
+  --tw-translate-y: 0;
+  --tw-rotate: 0;
+  --tw-skew-x: 0;
+  --tw-skew-y: 0;
+  --tw-scale-x: 1;
+  --tw-scale-y: 1;
+  transform: translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y))
+    rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y))
+    scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+.transform-gpu {
+  --tw-translate-x: 0;
+  --tw-translate-y: 0;
+  --tw-rotate: 0;
+  --tw-skew-x: 0;
+  --tw-skew-y: 0;
+  --tw-scale-x: 1;
+  --tw-scale-y: 1;
+  transform: translate3d(var(--tw-translate-x), var(--tw-translate-y), 0) rotate(var(--tw-rotate))
+    skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x))
+    scaleY(var(--tw-scale-y));
+}
+.origin-top-right {
+  transform-origin: top right;
+}
+.translate-x-5 {
+  --tw-translate-x: 1.25rem;
+}
+.-translate-x-4 {
+  --tw-translate-x: -1rem;
+}
+.translate-y-6 {
+  --tw-translate-y: 1.5rem;
+}
+.-translate-x-3 {
+  --tw-translate-x: -0.75rem;
+}
+.rotate-3 {
+  --tw-rotate: 3deg;
+}
+.skew-y-12 {
+  --tw-skew-y: 12deg;
+}
+.skew-x-12 {
+  --tw-skew-x: 12deg;
+}
+.scale-95 {
+  --tw-scale-x: 0.95;
+  --tw-scale-y: 0.95;
+}
+.animate-none {
+  animation: none;
+}
+@keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
+.animate-spin {
+  animation: spin 1s linear infinite;
+}
+.cursor-pointer {
+  cursor: pointer;
+}
+.select-none {
+  user-select: none;
+}
+.resize-none {
+  resize: none;
+}
+.list-inside {
+  list-style-position: inside;
+}
+.list-disc {
+  list-style-type: disc;
+}
+.appearance-none {
+  appearance: none;
+}
+.auto-cols-min {
+  grid-auto-columns: min-content;
+}
+.grid-flow-row {
+  grid-auto-flow: row;
+}
+.auto-rows-max {
+  grid-auto-rows: max-content;
+}
+.grid-cols-4 {
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+.grid-rows-3 {
+  grid-template-rows: repeat(3, minmax(0, 1fr));
+}
+.flex-row-reverse {
+  flex-direction: row-reverse;
+}
+.flex-wrap {
+  flex-wrap: wrap;
+}
+.place-content-start {
+  place-content: start;
+}
+.place-items-end {
+  place-items: end;
+}
+.content-center {
+  align-content: center;
+}
+.items-start {
+  align-items: flex-start;
+}
+.justify-center {
+  justify-content: center;
+}
+.justify-items-end {
+  justify-items: end;
+}
+.gap-4 {
+  gap: 1rem;
+}
+.gap-x-2 {
+  column-gap: 0.5rem;
+}
+.gap-y-3 {
+  row-gap: 0.75rem;
+}
+.space-x-4 > :not([hidden]) ~ :not([hidden]) {
+  --tw-space-x-reverse: 0;
+  margin-right: calc(1rem * var(--tw-space-x-reverse));
+  margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
+}
+.space-y-3 > :not([hidden]) ~ :not([hidden]) {
+  --tw-space-y-reverse: 0;
+  margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
+  margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
+}
+.space-y-reverse > :not([hidden]) ~ :not([hidden]) {
+  --tw-space-y-reverse: 1;
+}
+.space-x-reverse > :not([hidden]) ~ :not([hidden]) {
+  --tw-space-x-reverse: 1;
+}
+.divide-x-2 > :not([hidden]) ~ :not([hidden]) {
+  --tw-divide-x-reverse: 0;
+  border-right-width: calc(2px * var(--tw-divide-x-reverse));
+  border-left-width: calc(2px * calc(1 - var(--tw-divide-x-reverse)));
+}
+.divide-y-4 > :not([hidden]) ~ :not([hidden]) {
+  --tw-divide-y-reverse: 0;
+  border-top-width: calc(4px * calc(1 - var(--tw-divide-y-reverse)));
+  border-bottom-width: calc(4px * var(--tw-divide-y-reverse));
+}
+.divide-x-0 > :not([hidden]) ~ :not([hidden]) {
+  --tw-divide-x-reverse: 0;
+  border-right-width: calc(0px * var(--tw-divide-x-reverse));
+  border-left-width: calc(0px * calc(1 - var(--tw-divide-x-reverse)));
+}
+.divide-y-0 > :not([hidden]) ~ :not([hidden]) {
+  --tw-divide-y-reverse: 0;
+  border-top-width: calc(0px * calc(1 - var(--tw-divide-y-reverse)));
+  border-bottom-width: calc(0px * var(--tw-divide-y-reverse));
+}
+.divide-dotted > :not([hidden]) ~ :not([hidden]) {
+  border-style: dotted;
+}
+.divide-gray-200 > :not([hidden]) ~ :not([hidden]) {
+  --tw-divide-opacity: 1;
+  border-color: rgba(229, 231, 235, var(--tw-divide-opacity));
+}
+.divide-opacity-50 > :not([hidden]) ~ :not([hidden]) {
+  --tw-divide-opacity: 0.5;
+}
+.place-self-center {
+  place-self: center;
+}
+.self-end {
+  align-self: flex-end;
+}
+.justify-self-start {
+  justify-self: start;
+}
+.overflow-hidden {
+  overflow: hidden;
+}
+.overscroll-contain {
+  overscroll-behavior: contain;
+}
+.truncate {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.overflow-ellipsis {
+  text-overflow: ellipsis;
+}
+.whitespace-nowrap {
+  white-space: nowrap;
+}
+.break-words {
+  overflow-wrap: break-word;
+}
+.rounded-md {
+  border-radius: 0.375rem;
+}
+.border {
+  border-width: 1px;
+}
+.border-2 {
+  border-width: 2px;
+}
+.border-solid {
+  border-style: solid;
+}
+.border-black {
+  --tw-border-opacity: 1;
+  border-color: rgba(0, 0, 0, var(--tw-border-opacity));
+}
+.border-opacity-10 {
+  --tw-border-opacity: 0.1;
+}
+.bg-green-500 {
+  --tw-bg-opacity: 1;
+  background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
+}
+.bg-opacity-20 {
+  --tw-bg-opacity: 0.2;
+}
+.bg-gradient-to-r {
+  background-image: linear-gradient(to right, var(--tw-gradient-stops));
+}
+.from-red-300 {
+  --tw-gradient-from: #fca5a5;
+  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(252, 165, 165, 0));
+}
+.via-purple-200 {
+  --tw-gradient-stops: var(--tw-gradient-from), #ddd6fe,
+    var(--tw-gradient-to, rgba(221, 214, 254, 0));
+}
+.to-blue-400 {
+  --tw-gradient-to: #60a5fa;
+}
+.decoration-slice {
+  box-decoration-break: slice;
+}
+.decoration-clone {
+  box-decoration-break: clone;
+}
+.bg-cover {
+  background-size: cover;
+}
+.bg-local {
+  background-attachment: local;
+}
+.bg-clip-border {
+  background-clip: border-box;
+}
+.bg-top {
+  background-position: top;
+}
+.bg-no-repeat {
+  background-repeat: no-repeat;
+}
+.bg-origin-border {
+  background-origin: border-box;
+}
+.bg-origin-padding {
+  background-origin: padding-box;
+}
+.bg-origin-content {
+  background-origin: content-box;
+}
+.fill-current {
+  fill: currentColor;
+}
+.stroke-current {
+  stroke: currentColor;
+}
+.stroke-2 {
+  stroke-width: 2;
+}
+.object-cover {
+  object-fit: cover;
+}
+.object-bottom {
+  object-position: bottom;
+}
+.p-4 {
+  padding: 1rem;
+}
+.py-2 {
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+}
+.px-3 {
+  padding-left: 0.75rem;
+  padding-right: 0.75rem;
+}
+.pt-1 {
+  padding-top: 0.25rem;
+}
+.pr-2 {
+  padding-right: 0.5rem;
+}
+.pb-3 {
+  padding-bottom: 0.75rem;
+}
+.pl-4 {
+  padding-left: 1rem;
+}
+.text-center {
+  text-align: center;
+}
+.align-middle {
+  vertical-align: middle;
+}
+.font-sans {
+  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
+    'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
+    'Segoe UI Symbol', 'Noto Color Emoji';
+}
+.text-2xl {
+  font-size: 1.5rem;
+  line-height: 2rem;
+}
+.font-medium {
+  font-weight: 500;
+}
+.uppercase {
+  text-transform: uppercase;
+}
+.not-italic {
+  font-style: normal;
+}
+.ordinal,
+.slashed-zero,
+.lining-nums,
+.oldstyle-nums,
+.proportional-nums,
+.tabular-nums,
+.diagonal-fractions,
+.stacked-fractions {
+  --tw-ordinal: var(--tw-empty, /*!*/ /*!*/);
+  --tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/);
+  --tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/);
+  --tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/);
+  --tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/);
+  font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure)
+    var(--tw-numeric-spacing) var(--tw-numeric-fraction);
+}
+.ordinal {
+  --tw-ordinal: ordinal;
+}
+.tabular-nums {
+  --tw-numeric-spacing: tabular-nums;
+}
+.diagonal-fractions {
+  --tw-numeric-fraction: diagonal-fractions;
+}
+.leading-relaxed {
+  line-height: 1.625;
+}
+.leading-5 {
+  line-height: 1.25rem;
+}
+.tracking-tight {
+  letter-spacing: -0.025em;
+}
+.text-indigo-500 {
+  --tw-text-opacity: 1;
+  color: rgba(99, 102, 241, var(--tw-text-opacity));
+}
+.text-opacity-10 {
+  --tw-text-opacity: 0.1;
+}
+.underline {
+  text-decoration: underline;
+}
+.antialiased {
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+.placeholder-green-300::placeholder {
+  --tw-placeholder-opacity: 1;
+  color: rgba(110, 231, 183, var(--tw-placeholder-opacity));
+}
+.placeholder-opacity-60::placeholder {
+  --tw-placeholder-opacity: 0.6;
+}
+.opacity-90 {
+  opacity: 0.9;
+}
+.bg-blend-darken {
+  background-blend-mode: darken;
+}
+.bg-blend-difference {
+  background-blend-mode: difference;
+}
+.mix-blend-multiply {
+  mix-blend-mode: multiply;
+}
+.mix-blend-saturation {
+  mix-blend-mode: saturation;
+}
+.shadow {
+  --tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+    var(--tw-shadow);
+}
+.shadow-md {
+  --tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+    var(--tw-shadow);
+}
+.shadow-lg {
+  --tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+    var(--tw-shadow);
+}
+.outline-none {
+  outline: 2px solid transparent;
+  outline-offset: 2px;
+}
+.outline-black {
+  outline: 2px dotted black;
+  outline-offset: 2px;
+}
+.ring {
+  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width)
+    var(--tw-ring-offset-color);
+  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width))
+    var(--tw-ring-color);
+  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+}
+.ring-4 {
+  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width)
+    var(--tw-ring-offset-color);
+  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width))
+    var(--tw-ring-color);
+  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+}
+.ring-white {
+  --tw-ring-opacity: 1;
+  --tw-ring-color: rgba(255, 255, 255, var(--tw-ring-opacity));
+}
+.ring-opacity-40 {
+  --tw-ring-opacity: 0.4;
+}
+.ring-offset-2 {
+  --tw-ring-offset-width: 2px;
+}
+.ring-offset-blue-300 {
+  --tw-ring-offset-color: #93c5fd;
+}
+.filter {
+  --tw-blur: var(--tw-empty, /*!*/ /*!*/);
+  --tw-brightness: var(--tw-empty, /*!*/ /*!*/);
+  --tw-contrast: var(--tw-empty, /*!*/ /*!*/);
+  --tw-grayscale: var(--tw-empty, /*!*/ /*!*/);
+  --tw-hue-rotate: var(--tw-empty, /*!*/ /*!*/);
+  --tw-invert: var(--tw-empty, /*!*/ /*!*/);
+  --tw-saturate: var(--tw-empty, /*!*/ /*!*/);
+  --tw-sepia: var(--tw-empty, /*!*/ /*!*/);
+  --tw-drop-shadow: var(--tw-empty, /*!*/ /*!*/);
+  filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale)
+    var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
+}
+.filter-none {
+  filter: none;
+}
+.blur-md {
+  --tw-blur: blur(12px);
+}
+.brightness-150 {
+  --tw-brightness: brightness(1.5);
+}
+.contrast-50 {
+  --tw-contrast: contrast(0.5);
+}
+.drop-shadow-md {
+  --tw-drop-shadow: drop-shadow(0 4px 3px rgba(0, 0, 0, 0.07))
+    drop-shadow(0 2px 2px rgba(0, 0, 0, 0.06));
+}
+.grayscale {
+  --tw-grayscale: grayscale(100%);
+}
+.hue-rotate-60 {
+  --tw-hue-rotate: hue-rotate(60deg);
+}
+.invert {
+  --tw-invert: invert(100%);
+}
+.saturate-200 {
+  --tw-saturate: saturate(2);
+}
+.sepia {
+  --tw-sepia: sepia(100%);
+}
+.backdrop-filter {
+  --tw-backdrop-blur: var(--tw-empty, /*!*/ /*!*/);
+  --tw-backdrop-brightness: var(--tw-empty, /*!*/ /*!*/);
+  --tw-backdrop-contrast: var(--tw-empty, /*!*/ /*!*/);
+  --tw-backdrop-grayscale: var(--tw-empty, /*!*/ /*!*/);
+  --tw-backdrop-hue-rotate: var(--tw-empty, /*!*/ /*!*/);
+  --tw-backdrop-invert: var(--tw-empty, /*!*/ /*!*/);
+  --tw-backdrop-opacity: var(--tw-empty, /*!*/ /*!*/);
+  --tw-backdrop-saturate: var(--tw-empty, /*!*/ /*!*/);
+  --tw-backdrop-sepia: var(--tw-empty, /*!*/ /*!*/);
+  backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast)
+    var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert)
+    var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
+}
+.backdrop-filter-none {
+  backdrop-filter: none;
+}
+.backdrop-blur-lg {
+  --tw-backdrop-blur: blur(16px);
+}
+.backdrop-brightness-50 {
+  --tw-backdrop-brightness: brightness(0.5);
+}
+.backdrop-contrast-0 {
+  --tw-backdrop-contrast: contrast(0);
+}
+.backdrop-grayscale {
+  --tw-backdrop-grayscale: grayscale(100%);
+}
+.backdrop-hue-rotate-90 {
+  --tw-backdrop-hue-rotate: hue-rotate(90deg);
+}
+.backdrop-invert {
+  --tw-backdrop-invert: invert(100%);
+}
+.backdrop-opacity-75 {
+  --tw-backdrop-opacity: opacity(0.75);
+}
+.backdrop-saturate-150 {
+  --tw-backdrop-saturate: saturate(1.5);
+}
+.backdrop-sepia {
+  --tw-backdrop-sepia: sepia(100%);
+}
+.transition {
+  transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow,
+    transform, filter, backdrop-filter;
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  transition-duration: 150ms;
+}
+.transition-all {
+  transition-property: all;
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  transition-duration: 150ms;
+}
+.delay-300 {
+  transition-delay: 300ms;
+}
+.duration-200 {
+  transition-duration: 200ms;
+}
+.ease-in-out {
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+}
diff --git a/tests/jit/raw-content.test.html b/tests/jit/raw-content.test.html
new file mode 100644
index 00000000..1e97f5fd
--- /dev/null
+++ b/tests/jit/raw-content.test.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Title</title>
+    <link rel="stylesheet" href="./tailwind.css" />
+  </head>
+  <body>
+    <div class="sr-only"></div>
+    <div class="content-center"></div>
+    <div class="items-start"></div>
+    <div class="self-end"></div>
+    <div class="animate-none"></div>
+    <div class="animate-spin"></div>
+    <div class="appearance-none"></div>
+    <div class="bg-local"></div>
+    <div class="bg-clip-border"></div>
+    <div class="bg-green-500"></div>
+    <div class="bg-gradient-to-r"></div>
+    <div class="bg-opacity-20"></div>
+    <div class="bg-top"></div>
+    <div class="bg-no-repeat"></div>
+    <div class="bg-cover"></div>
+    <div class="bg-origin-border bg-origin-padding bg-origin-content"></div>
+    <div class="border-collapse"></div>
+    <div class="border-black"></div>
+    <div class="border-opacity-10"></div>
+    <div class="rounded-md"></div>
+    <div class="border-solid"></div>
+    <div class="border"></div>
+    <div class="border-2"></div>
+    <div class="shadow"></div>
+    <div class="shadow-md"></div>
+    <div class="shadow-lg"></div>
+    <div class="decoration-clone decoration-slice"></div>
+    <div class="box-border"></div>
+    <div class="clear-left"></div>
+    <div class="container"></div>
+    <div class="cursor-pointer"></div>
+    <div class="hidden inline-grid"></div>
+    <div class="divide-gray-200"></div>
+    <div class="divide-opacity-50"></div>
+    <div class="divide-dotted"></div>
+    <div class="divide-x-2 divide-y-4 divide-x-0 divide-y-0"></div>
+    <div class="fill-current"></div>
+    <div class="flex-1"></div>
+    <div class="flex-row-reverse"></div>
+    <div class="flex-grow"></div>
+    <div class="flex-grow-0"></div>
+    <div class="flex-shrink"></div>
+    <div class="flex-shrink-0"></div>
+    <div class="flex-wrap"></div>
+    <div class="float-right"></div>
+    <div class="font-sans"></div>
+    <div class="text-2xl"></div>
+    <div class="antialiased"></div>
+    <div class="not-italic"></div>
+    <div class="tabular-nums ordinal diagonal-fractions"></div>
+    <div class="font-medium"></div>
+    <div class="gap-x-2 gap-y-3 gap-4"></div>
+    <div class="from-red-300 via-purple-200 to-blue-400"></div>
+    <div class="auto-cols-min"></div>
+    <div class="grid-flow-row"></div>
+    <div class="auto-rows-max"></div>
+    <div class="col-span-3"></div>
+    <div class="col-start-1"></div>
+    <div class="col-end-4"></div>
+    <div class="row-span-2"></div>
+    <div class="row-start-3"></div>
+    <div class="row-end-5"></div>
+    <div class="grid-cols-4"></div>
+    <div class="grid-rows-3"></div>
+    <div class="h-16"></div>
+    <div class="inset-0 inset-y-4 inset-x-2 top-6 right-8 bottom-12 left-16"></div>
+    <div class="isolate isolation-auto"></div>
+    <div class="justify-center"></div>
+    <div class="justify-items-end"></div>
+    <div class="justify-self-start"></div>
+    <div class="tracking-tight"></div>
+    <div class="leading-relaxed leading-5"></div>
+    <div class="list-inside"></div>
+    <div class="list-disc"></div>
+    <div class="m-4 my-2 mx-auto mt-0 mr-1 mb-3 ml-4"></div>
+    <div class="max-h-screen"></div>
+    <div class="max-w-full"></div>
+    <div class="min-h-0"></div>
+    <div class="min-w-min"></div>
+    <div class="object-cover"></div>
+    <div class="object-bottom"></div>
+    <div class="opacity-90"></div>
+    <div class="bg-blend-darken bg-blend-difference"></div>
+    <div class="mix-blend-multiply mix-blend-saturation"></div>
+    <div class="order-last order-2"></div>
+    <div class="outline-none outline-black"></div>
+    <div class="overflow-hidden"></div>
+    <div class="overscroll-contain"></div>
+    <div class="p-4 py-2 px-3 pt-1 pr-2 pb-3 pl-4"></div>
+    <div class="place-content-start"></div>
+    <div class="placeholder-green-300"></div>
+    <div class="placeholder-opacity-60"></div>
+    <div class="place-items-end"></div>
+    <div class="place-self-center"></div>
+    <div class="pointer-events-none"></div>
+    <div class="absolute"></div>
+    <div class="resize-none"></div>
+    <div class="ring-white"></div>
+    <div class="ring-offset-blue-300"></div>
+    <div class="ring-offset-2"></div>
+    <div class="ring-opacity-40"></div>
+    <div class="ring ring-4"></div>
+    <div
+      class="filter filter-none blur-md brightness-150 contrast-50 drop-shadow-md grayscale hue-rotate-60 invert saturate-200 sepia"
+    ></div>
+    <div
+      class="backdrop-filter backdrop-filter-none backdrop-blur-lg backdrop-brightness-50 backdrop-contrast-0 backdrop-grayscale backdrop-hue-rotate-90 backdrop-invert backdrop-opacity-75 backdrop-saturate-150 backdrop-sepia"
+    ></div>
+    <div class="rotate-3"></div>
+    <div class="scale-95"></div>
+    <div class="skew-y-12 skew-x-12"></div>
+    <div class="space-x-4 space-y-3 space-x-reverse space-y-reverse"></div>
+    <div class="stroke-current"></div>
+    <div class="stroke-2"></div>
+    <div class="table-fixed"></div>
+    <div class="text-center"></div>
+    <div class="text-indigo-500"></div>
+    <div class="underline"></div>
+    <div class="text-opacity-10"></div>
+    <div class="overflow-ellipsis truncate"></div>
+    <div class="uppercase"></div>
+    <div class="transform transform-gpu"></div>
+    <div class="origin-top-right"></div>
+    <div class="delay-300"></div>
+    <div class="duration-200"></div>
+    <div class="transition transition-all"></div>
+    <div class="ease-in-out"></div>
+    <div class="translate-x-5 -translate-x-4 translate-y-6 -translate-x-3"></div>
+    <div class="select-none"></div>
+    <div class="align-middle"></div>
+    <div class="invisible"></div>
+    <div class="whitespace-nowrap"></div>
+    <div class="w-12"></div>
+    <div class="break-words"></div>
+    <div class="z-30"></div>
+  </body>
+</html>
diff --git a/tests/jit/raw-content.test.js b/tests/jit/raw-content.test.js
new file mode 100644
index 00000000..b699a22a
--- /dev/null
+++ b/tests/jit/raw-content.test.js
@@ -0,0 +1,85 @@
+import postcss from 'postcss'
+import fs from 'fs'
+import path from 'path'
+
+beforeEach(() => {
+  jest.resetModules()
+})
+
+function run(tailwind, input, config = {}) {
+  return postcss(tailwind(config)).process(input, {
+    from: path.resolve(__filename),
+  })
+}
+
+test('raw content', () => {
+  let tailwind = require('../../src/jit/index.js').default
+
+  let config = {
+    mode: 'jit',
+    purge: [{ raw: fs.readFileSync(path.resolve(__dirname, './raw-content.test.html'), 'utf8') }],
+    corePlugins: { preflight: false },
+    theme: {},
+    plugins: [],
+  }
+
+  let css = `
+    @tailwind base;
+    @tailwind components;
+    @tailwind utilities;
+  `
+
+  return run(tailwind, css, config).then((result) => {
+    let expectedPath = path.resolve(__dirname, './raw-content.test.css')
+    let expected = fs.readFileSync(expectedPath, 'utf8')
+
+    expect(result.css).toMatchFormattedCss(expected)
+  })
+})
+
+test('raw content with extension', () => {
+  let tailwind = require('../../src/jit/index.js').default
+
+  let config = {
+    mode: 'jit',
+    purge: {
+      content: [
+        {
+          raw: fs.readFileSync(path.resolve(__dirname, './raw-content.test.html'), 'utf8'),
+          extension: 'html',
+        },
+      ],
+      options: {
+        extractors: [
+          {
+            extractor: () => [],
+            extensions: ['html'],
+          },
+        ],
+      },
+    },
+    corePlugins: { preflight: false },
+    theme: {},
+    plugins: [],
+  }
+
+  let css = `
+    @tailwind base;
+    @tailwind components;
+    @tailwind utilities;
+  `
+
+  return run(tailwind, css, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(`
+      * {
+        --tw-shadow: 0 0 #0000;
+        --tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
+        --tw-ring-offset-width: 0px;
+        --tw-ring-offset-color: #fff;
+        --tw-ring-color: rgba(59, 130, 246, 0.5);
+        --tw-ring-offset-shadow: 0 0 #0000;
+        --tw-ring-shadow: 0 0 #0000;
+      }
+    `)
+  })
+})