diff --git a/CHANGELOG.md b/CHANGELOG.md
index ffef8f9702fa..e4e8eab5b5d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add support for `addBase` plugins using the `@plugin` directive ([#14172](https://github.com/tailwindlabs/tailwindcss/pull/14172))
- Add support for the `tailwindcss/plugin` export ([#14173](https://github.com/tailwindlabs/tailwindcss/pull/14173))
- Add support for the `theme()` function in plugins ([#14207](https://github.com/tailwindlabs/tailwindcss/pull/14207))
+- Add support for `addComponents`, `matchComponents`, `prefix` plugin APIs ([#14221](https://github.com/tailwindlabs/tailwindcss/pull/14221))
+- Add support for `tailwindcss/colors` and `tailwindcss/defaultTheme` exports for use with plugins ([#14221](https://github.com/tailwindlabs/tailwindcss/pull/14221))
+- Add support for the `@tailwindcss/typography` and `@tailwindcss/forms` plugins ([#14221](https://github.com/tailwindlabs/tailwindcss/pull/14221))
### Fixed
diff --git a/integrations/cli/plugins.test.ts b/integrations/cli/plugins.test.ts
new file mode 100644
index 000000000000..9a17d6dbf6f9
--- /dev/null
+++ b/integrations/cli/plugins.test.ts
@@ -0,0 +1,78 @@
+import { candidate, css, html, json, test } from '../utils'
+
+test(
+ 'builds the typography plugin utilities',
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "dependencies": {
+ "@tailwindcss/typography": "^0.5.14",
+ "tailwindcss": "workspace:^",
+ "@tailwindcss/cli": "workspace:^"
+ }
+ }
+ `,
+ 'index.html': html`
+
+
Headline
+
+ Until now, trying to style an article, document, or blog post with Tailwind has been a
+ tedious task that required a keen eye for typography and a lot of complex custom CSS.
+
+
+ `,
+ 'src/index.css': css`
+ @import 'tailwindcss';
+ @plugin '@tailwindcss/typography';
+ `,
+ },
+ },
+ async ({ fs, exec }) => {
+ await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
+
+ await fs.expectFileToContain('dist/out.css', [
+ candidate`prose`,
+ ':where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *))',
+ ':where(tbody td, tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *))',
+ ])
+ },
+)
+
+test(
+ 'builds the forms plugin utilities',
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "dependencies": {
+ "@tailwindcss/forms": "^0.5.7",
+ "tailwindcss": "workspace:^",
+ "@tailwindcss/cli": "workspace:^"
+ }
+ }
+ `,
+ 'index.html': html`
+
+
+ `,
+ 'src/index.css': css`
+ @import 'tailwindcss';
+ @plugin '@tailwindcss/forms';
+ `,
+ },
+ },
+ async ({ fs, exec }) => {
+ await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
+
+ await fs.expectFileToContain('dist/out.css', [
+ //
+ candidate`form-input`,
+ candidate`form-textarea`,
+ ])
+ await fs.expectFileNotToContain('dist/out.css', [
+ //
+ candidate`form-radio`,
+ ])
+ },
+)
diff --git a/integrations/utils.ts b/integrations/utils.ts
index 144b69e1e8fb..71c9eec02abb 100644
--- a/integrations/utils.ts
+++ b/integrations/utils.ts
@@ -38,6 +38,7 @@ interface TestContext {
read(filePath: string): Promise
glob(pattern: string): Promise<[string, string][]>
expectFileToContain(filePath: string, contents: string | string[]): Promise
+ expectFileNotToContain(filePath: string, contents: string | string[]): Promise
}
}
type TestCallback = (context: TestContext) => Promise | void
@@ -289,9 +290,21 @@ export function test(
}
})
},
+ async expectFileNotToContain(filePath, contents) {
+ return retryAssertion(async () => {
+ let fileContent = await this.read(filePath)
+ for (let content of contents) {
+ expect(fileContent).not.toContain(content)
+ }
+ })
+ },
},
} satisfies TestContext
+ config.fs['.gitignore'] ??= txt`
+ node_modules/
+ `
+
for (let [filename, content] of Object.entries(config.fs)) {
await context.fs.write(filename, content)
}
diff --git a/packages/tailwindcss/package.json b/packages/tailwindcss/package.json
index 5bd5b7922325..4a6c4b26e76f 100644
--- a/packages/tailwindcss/package.json
+++ b/packages/tailwindcss/package.json
@@ -23,6 +23,14 @@
"require": "./dist/lib.js",
"import": "./src/index.ts"
},
+ "./colors": {
+ "require": "./src/compat/colors.cts",
+ "import": "./src/compat/colors.ts"
+ },
+ "./defaultTheme": {
+ "require": "./src/compat/default-theme.cts",
+ "import": "./src/compat/default-theme.ts"
+ },
"./plugin": {
"require": "./src/plugin.cts",
"import": "./src/plugin.ts"
@@ -49,7 +57,15 @@
},
"./plugin": {
"require": "./dist/plugin.js",
- "import": "./src/plugin.mjs"
+ "import": "./dist/plugin.mjs"
+ },
+ "./defaultTheme": {
+ "require": "./dist/default-theme.js",
+ "import": "./dist/default-theme.mjs"
+ },
+ "./colors": {
+ "require": "./dist/colors.js",
+ "import": "./dist/colors.mjs"
},
"./package.json": "./package.json",
"./index.css": "./index.css",
diff --git a/packages/tailwindcss/src/compat/colors.cts b/packages/tailwindcss/src/compat/colors.cts
new file mode 100644
index 000000000000..34d90e1e965d
--- /dev/null
+++ b/packages/tailwindcss/src/compat/colors.cts
@@ -0,0 +1 @@
+module.exports = require('./colors.ts').default
diff --git a/packages/tailwindcss/src/compat/colors.ts b/packages/tailwindcss/src/compat/colors.ts
new file mode 100644
index 000000000000..9489134e240f
--- /dev/null
+++ b/packages/tailwindcss/src/compat/colors.ts
@@ -0,0 +1,293 @@
+export default {
+ inherit: 'inherit',
+ current: 'currentColor',
+ transparent: 'transparent',
+ black: '#000',
+ white: '#fff',
+ slate: {
+ 50: '#f8fafc',
+ 100: '#f1f5f9',
+ 200: '#e2e8f0',
+ 300: '#cbd5e1',
+ 400: '#94a3b8',
+ 500: '#64748b',
+ 600: '#475569',
+ 700: '#334155',
+ 800: '#1e293b',
+ 900: '#0f172a',
+ 950: '#020617',
+ },
+ gray: {
+ 50: '#f9fafb',
+ 100: '#f3f4f6',
+ 200: '#e5e7eb',
+ 300: '#d1d5db',
+ 400: '#9ca3af',
+ 500: '#6b7280',
+ 600: '#4b5563',
+ 700: '#374151',
+ 800: '#1f2937',
+ 900: '#111827',
+ 950: '#030712',
+ },
+ zinc: {
+ 50: '#fafafa',
+ 100: '#f4f4f5',
+ 200: '#e4e4e7',
+ 300: '#d4d4d8',
+ 400: '#a1a1aa',
+ 500: '#71717a',
+ 600: '#52525b',
+ 700: '#3f3f46',
+ 800: '#27272a',
+ 900: '#18181b',
+ 950: '#09090b',
+ },
+ neutral: {
+ 50: '#fafafa',
+ 100: '#f5f5f5',
+ 200: '#e5e5e5',
+ 300: '#d4d4d4',
+ 400: '#a3a3a3',
+ 500: '#737373',
+ 600: '#525252',
+ 700: '#404040',
+ 800: '#262626',
+ 900: '#171717',
+ 950: '#0a0a0a',
+ },
+ stone: {
+ 50: '#fafaf9',
+ 100: '#f5f5f4',
+ 200: '#e7e5e4',
+ 300: '#d6d3d1',
+ 400: '#a8a29e',
+ 500: '#78716c',
+ 600: '#57534e',
+ 700: '#44403c',
+ 800: '#292524',
+ 900: '#1c1917',
+ 950: '#0c0a09',
+ },
+ red: {
+ 50: '#fef2f2',
+ 100: '#fee2e2',
+ 200: '#fecaca',
+ 300: '#fca5a5',
+ 400: '#f87171',
+ 500: '#ef4444',
+ 600: '#dc2626',
+ 700: '#b91c1c',
+ 800: '#991b1b',
+ 900: '#7f1d1d',
+ 950: '#450a0a',
+ },
+ orange: {
+ 50: '#fff7ed',
+ 100: '#ffedd5',
+ 200: '#fed7aa',
+ 300: '#fdba74',
+ 400: '#fb923c',
+ 500: '#f97316',
+ 600: '#ea580c',
+ 700: '#c2410c',
+ 800: '#9a3412',
+ 900: '#7c2d12',
+ 950: '#431407',
+ },
+ amber: {
+ 50: '#fffbeb',
+ 100: '#fef3c7',
+ 200: '#fde68a',
+ 300: '#fcd34d',
+ 400: '#fbbf24',
+ 500: '#f59e0b',
+ 600: '#d97706',
+ 700: '#b45309',
+ 800: '#92400e',
+ 900: '#78350f',
+ 950: '#451a03',
+ },
+ yellow: {
+ 50: '#fefce8',
+ 100: '#fef9c3',
+ 200: '#fef08a',
+ 300: '#fde047',
+ 400: '#facc15',
+ 500: '#eab308',
+ 600: '#ca8a04',
+ 700: '#a16207',
+ 800: '#854d0e',
+ 900: '#713f12',
+ 950: '#422006',
+ },
+ lime: {
+ 50: '#f7fee7',
+ 100: '#ecfccb',
+ 200: '#d9f99d',
+ 300: '#bef264',
+ 400: '#a3e635',
+ 500: '#84cc16',
+ 600: '#65a30d',
+ 700: '#4d7c0f',
+ 800: '#3f6212',
+ 900: '#365314',
+ 950: '#1a2e05',
+ },
+ green: {
+ 50: '#f0fdf4',
+ 100: '#dcfce7',
+ 200: '#bbf7d0',
+ 300: '#86efac',
+ 400: '#4ade80',
+ 500: '#22c55e',
+ 600: '#16a34a',
+ 700: '#15803d',
+ 800: '#166534',
+ 900: '#14532d',
+ 950: '#052e16',
+ },
+ emerald: {
+ 50: '#ecfdf5',
+ 100: '#d1fae5',
+ 200: '#a7f3d0',
+ 300: '#6ee7b7',
+ 400: '#34d399',
+ 500: '#10b981',
+ 600: '#059669',
+ 700: '#047857',
+ 800: '#065f46',
+ 900: '#064e3b',
+ 950: '#022c22',
+ },
+ teal: {
+ 50: '#f0fdfa',
+ 100: '#ccfbf1',
+ 200: '#99f6e4',
+ 300: '#5eead4',
+ 400: '#2dd4bf',
+ 500: '#14b8a6',
+ 600: '#0d9488',
+ 700: '#0f766e',
+ 800: '#115e59',
+ 900: '#134e4a',
+ 950: '#042f2e',
+ },
+ cyan: {
+ 50: '#ecfeff',
+ 100: '#cffafe',
+ 200: '#a5f3fc',
+ 300: '#67e8f9',
+ 400: '#22d3ee',
+ 500: '#06b6d4',
+ 600: '#0891b2',
+ 700: '#0e7490',
+ 800: '#155e75',
+ 900: '#164e63',
+ 950: '#083344',
+ },
+ sky: {
+ 50: '#f0f9ff',
+ 100: '#e0f2fe',
+ 200: '#bae6fd',
+ 300: '#7dd3fc',
+ 400: '#38bdf8',
+ 500: '#0ea5e9',
+ 600: '#0284c7',
+ 700: '#0369a1',
+ 800: '#075985',
+ 900: '#0c4a6e',
+ 950: '#082f49',
+ },
+ blue: {
+ 50: '#eff6ff',
+ 100: '#dbeafe',
+ 200: '#bfdbfe',
+ 300: '#93c5fd',
+ 400: '#60a5fa',
+ 500: '#3b82f6',
+ 600: '#2563eb',
+ 700: '#1d4ed8',
+ 800: '#1e40af',
+ 900: '#1e3a8a',
+ 950: '#172554',
+ },
+ indigo: {
+ 50: '#eef2ff',
+ 100: '#e0e7ff',
+ 200: '#c7d2fe',
+ 300: '#a5b4fc',
+ 400: '#818cf8',
+ 500: '#6366f1',
+ 600: '#4f46e5',
+ 700: '#4338ca',
+ 800: '#3730a3',
+ 900: '#312e81',
+ 950: '#1e1b4b',
+ },
+ violet: {
+ 50: '#f5f3ff',
+ 100: '#ede9fe',
+ 200: '#ddd6fe',
+ 300: '#c4b5fd',
+ 400: '#a78bfa',
+ 500: '#8b5cf6',
+ 600: '#7c3aed',
+ 700: '#6d28d9',
+ 800: '#5b21b6',
+ 900: '#4c1d95',
+ 950: '#2e1065',
+ },
+ purple: {
+ 50: '#faf5ff',
+ 100: '#f3e8ff',
+ 200: '#e9d5ff',
+ 300: '#d8b4fe',
+ 400: '#c084fc',
+ 500: '#a855f7',
+ 600: '#9333ea',
+ 700: '#7e22ce',
+ 800: '#6b21a8',
+ 900: '#581c87',
+ 950: '#3b0764',
+ },
+ fuchsia: {
+ 50: '#fdf4ff',
+ 100: '#fae8ff',
+ 200: '#f5d0fe',
+ 300: '#f0abfc',
+ 400: '#e879f9',
+ 500: '#d946ef',
+ 600: '#c026d3',
+ 700: '#a21caf',
+ 800: '#86198f',
+ 900: '#701a75',
+ 950: '#4a044e',
+ },
+ pink: {
+ 50: '#fdf2f8',
+ 100: '#fce7f3',
+ 200: '#fbcfe8',
+ 300: '#f9a8d4',
+ 400: '#f472b6',
+ 500: '#ec4899',
+ 600: '#db2777',
+ 700: '#be185d',
+ 800: '#9d174d',
+ 900: '#831843',
+ 950: '#500724',
+ },
+ rose: {
+ 50: '#fff1f2',
+ 100: '#ffe4e6',
+ 200: '#fecdd3',
+ 300: '#fda4af',
+ 400: '#fb7185',
+ 500: '#f43f5e',
+ 600: '#e11d48',
+ 700: '#be123c',
+ 800: '#9f1239',
+ 900: '#881337',
+ 950: '#4c0519',
+ },
+}
diff --git a/packages/tailwindcss/src/compat/default-theme.cts b/packages/tailwindcss/src/compat/default-theme.cts
new file mode 100644
index 000000000000..bc1750d7efec
--- /dev/null
+++ b/packages/tailwindcss/src/compat/default-theme.cts
@@ -0,0 +1 @@
+module.exports = require('./default-theme.ts').default
diff --git a/packages/tailwindcss/src/compat/default-theme.ts b/packages/tailwindcss/src/compat/default-theme.ts
new file mode 100644
index 000000000000..c64b7136cc46
--- /dev/null
+++ b/packages/tailwindcss/src/compat/default-theme.ts
@@ -0,0 +1,763 @@
+import { Theme } from '../theme'
+import { createCompatConfig } from './config/create-compat-config'
+
+let theme = new Theme()
+
+export default {
+ ...createCompatConfig(theme).theme,
+ animation: {
+ none: 'none',
+ spin: 'spin 1s linear infinite',
+ ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
+ pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
+ bounce: 'bounce 1s infinite',
+ },
+ aria: {
+ busy: 'busy="true"',
+ checked: 'checked="true"',
+ disabled: 'disabled="true"',
+ expanded: 'expanded="true"',
+ hidden: 'hidden="true"',
+ pressed: 'pressed="true"',
+ readonly: 'readonly="true"',
+ required: 'required="true"',
+ selected: 'selected="true"',
+ },
+ aspectRatio: {
+ auto: 'auto',
+ square: '1 / 1',
+ video: '16 / 9',
+ },
+ backgroundImage: {
+ none: 'none',
+ 'gradient-to-t': 'linear-gradient(to top, var(--tw-gradient-stops))',
+ 'gradient-to-tr': 'linear-gradient(to top right, var(--tw-gradient-stops))',
+ 'gradient-to-r': 'linear-gradient(to right, var(--tw-gradient-stops))',
+ 'gradient-to-br': 'linear-gradient(to bottom right, var(--tw-gradient-stops))',
+ 'gradient-to-b': 'linear-gradient(to bottom, var(--tw-gradient-stops))',
+ 'gradient-to-bl': 'linear-gradient(to bottom left, var(--tw-gradient-stops))',
+ 'gradient-to-l': 'linear-gradient(to left, var(--tw-gradient-stops))',
+ 'gradient-to-tl': 'linear-gradient(to top left, var(--tw-gradient-stops))',
+ },
+ backgroundPosition: {
+ bottom: 'bottom',
+ center: 'center',
+ left: 'left',
+ 'left-bottom': 'left bottom',
+ 'left-top': 'left top',
+ right: 'right',
+ 'right-bottom': 'right bottom',
+ 'right-top': 'right top',
+ top: 'top',
+ },
+ backgroundSize: {
+ auto: 'auto',
+ cover: 'cover',
+ contain: 'contain',
+ },
+ blur: {
+ 0: '0',
+ none: '',
+ sm: '4px',
+ DEFAULT: '8px',
+ md: '12px',
+ lg: '16px',
+ xl: '24px',
+ '2xl': '40px',
+ '3xl': '64px',
+ },
+ borderRadius: {
+ none: '0px',
+ sm: '0.125rem',
+ DEFAULT: '0.25rem',
+ md: '0.375rem',
+ lg: '0.5rem',
+ xl: '0.75rem',
+ '2xl': '1rem',
+ '3xl': '1.5rem',
+ full: '9999px',
+ },
+ borderWidth: {
+ DEFAULT: '1px',
+ 0: '0px',
+ 2: '2px',
+ 4: '4px',
+ 8: '8px',
+ },
+ boxShadow: {
+ sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
+ DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
+ md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
+ lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
+ xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
+ '2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)',
+ inner: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)',
+ none: 'none',
+ },
+ brightness: {
+ 0: '0',
+ 50: '.5',
+ 75: '.75',
+ 90: '.9',
+ 95: '.95',
+ 100: '1',
+ 105: '1.05',
+ 110: '1.1',
+ 125: '1.25',
+ 150: '1.5',
+ 200: '2',
+ },
+ columns: {
+ auto: 'auto',
+ 1: '1',
+ 2: '2',
+ 3: '3',
+ 4: '4',
+ 5: '5',
+ 6: '6',
+ 7: '7',
+ 8: '8',
+ 9: '9',
+ 10: '10',
+ 11: '11',
+ 12: '12',
+ '3xs': '16rem',
+ '2xs': '18rem',
+ xs: '20rem',
+ sm: '24rem',
+ md: '28rem',
+ lg: '32rem',
+ xl: '36rem',
+ '2xl': '42rem',
+ '3xl': '48rem',
+ '4xl': '56rem',
+ '5xl': '64rem',
+ '6xl': '72rem',
+ '7xl': '80rem',
+ },
+ container: {},
+ content: {
+ none: 'none',
+ },
+ contrast: {
+ 0: '0',
+ 50: '.5',
+ 75: '.75',
+ 100: '1',
+ 125: '1.25',
+ 150: '1.5',
+ 200: '2',
+ },
+ cursor: {
+ auto: 'auto',
+ default: 'default',
+ pointer: 'pointer',
+ wait: 'wait',
+ text: 'text',
+ move: 'move',
+ help: 'help',
+ 'not-allowed': 'not-allowed',
+ none: 'none',
+ 'context-menu': 'context-menu',
+ progress: 'progress',
+ cell: 'cell',
+ crosshair: 'crosshair',
+ 'vertical-text': 'vertical-text',
+ alias: 'alias',
+ copy: 'copy',
+ 'no-drop': 'no-drop',
+ grab: 'grab',
+ grabbing: 'grabbing',
+ 'all-scroll': 'all-scroll',
+ 'col-resize': 'col-resize',
+ 'row-resize': 'row-resize',
+ 'n-resize': 'n-resize',
+ 'e-resize': 'e-resize',
+ 's-resize': 's-resize',
+ 'w-resize': 'w-resize',
+ 'ne-resize': 'ne-resize',
+ 'nw-resize': 'nw-resize',
+ 'se-resize': 'se-resize',
+ 'sw-resize': 'sw-resize',
+ 'ew-resize': 'ew-resize',
+ 'ns-resize': 'ns-resize',
+ 'nesw-resize': 'nesw-resize',
+ 'nwse-resize': 'nwse-resize',
+ 'zoom-in': 'zoom-in',
+ 'zoom-out': 'zoom-out',
+ },
+ dropShadow: {
+ sm: '0 1px 1px rgb(0 0 0 / 0.05)',
+ DEFAULT: ['0 1px 2px rgb(0 0 0 / 0.1)', '0 1px 1px rgb(0 0 0 / 0.06)'],
+ md: ['0 4px 3px rgb(0 0 0 / 0.07)', '0 2px 2px rgb(0 0 0 / 0.06)'],
+ lg: ['0 10px 8px rgb(0 0 0 / 0.04)', '0 4px 3px rgb(0 0 0 / 0.1)'],
+ xl: ['0 20px 13px rgb(0 0 0 / 0.03)', '0 8px 5px rgb(0 0 0 / 0.08)'],
+ '2xl': '0 25px 25px rgb(0 0 0 / 0.15)',
+ none: '0 0 #0000',
+ },
+ flex: {
+ 1: '1 1 0%',
+ auto: '1 1 auto',
+ initial: '0 1 auto',
+ none: 'none',
+ },
+ flexGrow: {
+ 0: '0',
+ DEFAULT: '1',
+ },
+ flexShrink: {
+ 0: '0',
+ DEFAULT: '1',
+ },
+ fontFamily: {
+ sans: [
+ 'ui-sans-serif',
+ 'system-ui',
+ 'sans-serif',
+ '"Apple Color Emoji"',
+ '"Segoe UI Emoji"',
+ '"Segoe UI Symbol"',
+ '"Noto Color Emoji"',
+ ],
+ serif: ['ui-serif', 'Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'],
+ mono: [
+ 'ui-monospace',
+ 'SFMono-Regular',
+ 'Menlo',
+ 'Monaco',
+ 'Consolas',
+ '"Liberation Mono"',
+ '"Courier New"',
+ 'monospace',
+ ],
+ },
+ fontSize: {
+ xs: ['0.75rem', { lineHeight: '1rem' }],
+ sm: ['0.875rem', { lineHeight: '1.25rem' }],
+ base: ['1rem', { lineHeight: '1.5rem' }],
+ lg: ['1.125rem', { lineHeight: '1.75rem' }],
+ xl: ['1.25rem', { lineHeight: '1.75rem' }],
+ '2xl': ['1.5rem', { lineHeight: '2rem' }],
+ '3xl': ['1.875rem', { lineHeight: '2.25rem' }],
+ '4xl': ['2.25rem', { lineHeight: '2.5rem' }],
+ '5xl': ['3rem', { lineHeight: '1' }],
+ '6xl': ['3.75rem', { lineHeight: '1' }],
+ '7xl': ['4.5rem', { lineHeight: '1' }],
+ '8xl': ['6rem', { lineHeight: '1' }],
+ '9xl': ['8rem', { lineHeight: '1' }],
+ },
+ fontWeight: {
+ thin: '100',
+ extralight: '200',
+ light: '300',
+ normal: '400',
+ medium: '500',
+ semibold: '600',
+ bold: '700',
+ extrabold: '800',
+ black: '900',
+ },
+ gradientColorStopPositions: {
+ '0%': '0%',
+ '5%': '5%',
+ '10%': '10%',
+ '15%': '15%',
+ '20%': '20%',
+ '25%': '25%',
+ '30%': '30%',
+ '35%': '35%',
+ '40%': '40%',
+ '45%': '45%',
+ '50%': '50%',
+ '55%': '55%',
+ '60%': '60%',
+ '65%': '65%',
+ '70%': '70%',
+ '75%': '75%',
+ '80%': '80%',
+ '85%': '85%',
+ '90%': '90%',
+ '95%': '95%',
+ '100%': '100%',
+ },
+ grayscale: {
+ 0: '0',
+ DEFAULT: '100%',
+ },
+ gridAutoColumns: {
+ auto: 'auto',
+ min: 'min-content',
+ max: 'max-content',
+ fr: 'minmax(0, 1fr)',
+ },
+ gridAutoRows: {
+ auto: 'auto',
+ min: 'min-content',
+ max: 'max-content',
+ fr: 'minmax(0, 1fr)',
+ },
+ gridColumn: {
+ auto: 'auto',
+ 'span-1': 'span 1 / span 1',
+ 'span-2': 'span 2 / span 2',
+ 'span-3': 'span 3 / span 3',
+ 'span-4': 'span 4 / span 4',
+ 'span-5': 'span 5 / span 5',
+ 'span-6': 'span 6 / span 6',
+ 'span-7': 'span 7 / span 7',
+ 'span-8': 'span 8 / span 8',
+ 'span-9': 'span 9 / span 9',
+ 'span-10': 'span 10 / span 10',
+ 'span-11': 'span 11 / span 11',
+ 'span-12': 'span 12 / span 12',
+ 'span-full': '1 / -1',
+ },
+ gridColumnEnd: {
+ auto: 'auto',
+ 1: '1',
+ 2: '2',
+ 3: '3',
+ 4: '4',
+ 5: '5',
+ 6: '6',
+ 7: '7',
+ 8: '8',
+ 9: '9',
+ 10: '10',
+ 11: '11',
+ 12: '12',
+ 13: '13',
+ },
+ gridColumnStart: {
+ auto: 'auto',
+ 1: '1',
+ 2: '2',
+ 3: '3',
+ 4: '4',
+ 5: '5',
+ 6: '6',
+ 7: '7',
+ 8: '8',
+ 9: '9',
+ 10: '10',
+ 11: '11',
+ 12: '12',
+ 13: '13',
+ },
+ gridRow: {
+ auto: 'auto',
+ 'span-1': 'span 1 / span 1',
+ 'span-2': 'span 2 / span 2',
+ 'span-3': 'span 3 / span 3',
+ 'span-4': 'span 4 / span 4',
+ 'span-5': 'span 5 / span 5',
+ 'span-6': 'span 6 / span 6',
+ 'span-7': 'span 7 / span 7',
+ 'span-8': 'span 8 / span 8',
+ 'span-9': 'span 9 / span 9',
+ 'span-10': 'span 10 / span 10',
+ 'span-11': 'span 11 / span 11',
+ 'span-12': 'span 12 / span 12',
+ 'span-full': '1 / -1',
+ },
+ gridRowEnd: {
+ auto: 'auto',
+ 1: '1',
+ 2: '2',
+ 3: '3',
+ 4: '4',
+ 5: '5',
+ 6: '6',
+ 7: '7',
+ 8: '8',
+ 9: '9',
+ 10: '10',
+ 11: '11',
+ 12: '12',
+ 13: '13',
+ },
+ gridRowStart: {
+ auto: 'auto',
+ 1: '1',
+ 2: '2',
+ 3: '3',
+ 4: '4',
+ 5: '5',
+ 6: '6',
+ 7: '7',
+ 8: '8',
+ 9: '9',
+ 10: '10',
+ 11: '11',
+ 12: '12',
+ 13: '13',
+ },
+ gridTemplateColumns: {
+ none: 'none',
+ subgrid: 'subgrid',
+ 1: 'repeat(1, minmax(0, 1fr))',
+ 2: 'repeat(2, minmax(0, 1fr))',
+ 3: 'repeat(3, minmax(0, 1fr))',
+ 4: 'repeat(4, minmax(0, 1fr))',
+ 5: 'repeat(5, minmax(0, 1fr))',
+ 6: 'repeat(6, minmax(0, 1fr))',
+ 7: 'repeat(7, minmax(0, 1fr))',
+ 8: 'repeat(8, minmax(0, 1fr))',
+ 9: 'repeat(9, minmax(0, 1fr))',
+ 10: 'repeat(10, minmax(0, 1fr))',
+ 11: 'repeat(11, minmax(0, 1fr))',
+ 12: 'repeat(12, minmax(0, 1fr))',
+ },
+ gridTemplateRows: {
+ none: 'none',
+ subgrid: 'subgrid',
+ 1: 'repeat(1, minmax(0, 1fr))',
+ 2: 'repeat(2, minmax(0, 1fr))',
+ 3: 'repeat(3, minmax(0, 1fr))',
+ 4: 'repeat(4, minmax(0, 1fr))',
+ 5: 'repeat(5, minmax(0, 1fr))',
+ 6: 'repeat(6, minmax(0, 1fr))',
+ 7: 'repeat(7, minmax(0, 1fr))',
+ 8: 'repeat(8, minmax(0, 1fr))',
+ 9: 'repeat(9, minmax(0, 1fr))',
+ 10: 'repeat(10, minmax(0, 1fr))',
+ 11: 'repeat(11, minmax(0, 1fr))',
+ 12: 'repeat(12, minmax(0, 1fr))',
+ },
+ hueRotate: {
+ 0: '0deg',
+ 15: '15deg',
+ 30: '30deg',
+ 60: '60deg',
+ 90: '90deg',
+ 180: '180deg',
+ },
+ invert: {
+ 0: '0',
+ DEFAULT: '100%',
+ },
+ keyframes: {
+ spin: {
+ to: {
+ transform: 'rotate(360deg)',
+ },
+ },
+ ping: {
+ '75%, 100%': {
+ transform: 'scale(2)',
+ opacity: '0',
+ },
+ },
+ pulse: {
+ '50%': {
+ opacity: '.5',
+ },
+ },
+ bounce: {
+ '0%, 100%': {
+ transform: 'translateY(-25%)',
+ animationTimingFunction: 'cubic-bezier(0.8,0,1,1)',
+ },
+ '50%': {
+ transform: 'none',
+ animationTimingFunction: 'cubic-bezier(0,0,0.2,1)',
+ },
+ },
+ },
+ letterSpacing: {
+ tighter: '-0.05em',
+ tight: '-0.025em',
+ normal: '0em',
+ wide: '0.025em',
+ wider: '0.05em',
+ widest: '0.1em',
+ },
+ lineHeight: {
+ none: '1',
+ tight: '1.25',
+ snug: '1.375',
+ normal: '1.5',
+ relaxed: '1.625',
+ loose: '2',
+ 3: '.75rem',
+ 4: '1rem',
+ 5: '1.25rem',
+ 6: '1.5rem',
+ 7: '1.75rem',
+ 8: '2rem',
+ 9: '2.25rem',
+ 10: '2.5rem',
+ },
+ listStyleType: {
+ none: 'none',
+ disc: 'disc',
+ decimal: 'decimal',
+ },
+ listStyleImage: {
+ none: 'none',
+ },
+ lineClamp: {
+ 1: '1',
+ 2: '2',
+ 3: '3',
+ 4: '4',
+ 5: '5',
+ 6: '6',
+ },
+ objectPosition: {
+ bottom: 'bottom',
+ center: 'center',
+ left: 'left',
+ 'left-bottom': 'left bottom',
+ 'left-top': 'left top',
+ right: 'right',
+ 'right-bottom': 'right bottom',
+ 'right-top': 'right top',
+ top: 'top',
+ },
+ opacity: {
+ 0: '0',
+ 5: '0.05',
+ 10: '0.1',
+ 15: '0.15',
+ 20: '0.2',
+ 25: '0.25',
+ 30: '0.3',
+ 35: '0.35',
+ 40: '0.4',
+ 45: '0.45',
+ 50: '0.5',
+ 55: '0.55',
+ 60: '0.6',
+ 65: '0.65',
+ 70: '0.7',
+ 75: '0.75',
+ 80: '0.8',
+ 85: '0.85',
+ 90: '0.9',
+ 95: '0.95',
+ 100: '1',
+ },
+ order: {
+ first: '-9999',
+ last: '9999',
+ none: '0',
+ 1: '1',
+ 2: '2',
+ 3: '3',
+ 4: '4',
+ 5: '5',
+ 6: '6',
+ 7: '7',
+ 8: '8',
+ 9: '9',
+ 10: '10',
+ 11: '11',
+ 12: '12',
+ },
+ outlineOffset: {
+ 0: '0px',
+ 1: '1px',
+ 2: '2px',
+ 4: '4px',
+ 8: '8px',
+ },
+ outlineWidth: {
+ 0: '0px',
+ 1: '1px',
+ 2: '2px',
+ 4: '4px',
+ 8: '8px',
+ },
+ ringOffsetWidth: {
+ 0: '0px',
+ 1: '1px',
+ 2: '2px',
+ 4: '4px',
+ 8: '8px',
+ },
+ ringWidth: {
+ DEFAULT: '3px',
+ 0: '0px',
+ 1: '1px',
+ 2: '2px',
+ 4: '4px',
+ 8: '8px',
+ },
+ rotate: {
+ 0: '0deg',
+ 1: '1deg',
+ 2: '2deg',
+ 3: '3deg',
+ 6: '6deg',
+ 12: '12deg',
+ 45: '45deg',
+ 90: '90deg',
+ 180: '180deg',
+ },
+ saturate: {
+ 0: '0',
+ 50: '.5',
+ 100: '1',
+ 150: '1.5',
+ 200: '2',
+ },
+ scale: {
+ 0: '0',
+ 50: '.5',
+ 75: '.75',
+ 90: '.9',
+ 95: '.95',
+ 100: '1',
+ 105: '1.05',
+ 110: '1.1',
+ 125: '1.25',
+ 150: '1.5',
+ },
+ screens: {
+ sm: '640px',
+ md: '768px',
+ lg: '1024px',
+ xl: '1280px',
+ '2xl': '1536px',
+ },
+ sepia: {
+ 0: '0',
+ DEFAULT: '100%',
+ },
+ skew: {
+ 0: '0deg',
+ 1: '1deg',
+ 2: '2deg',
+ 3: '3deg',
+ 6: '6deg',
+ 12: '12deg',
+ },
+ spacing: {
+ px: '1px',
+ 0: '0px',
+ 0.5: '0.125rem',
+ 1: '0.25rem',
+ 1.5: '0.375rem',
+ 2: '0.5rem',
+ 2.5: '0.625rem',
+ 3: '0.75rem',
+ 3.5: '0.875rem',
+ 4: '1rem',
+ 5: '1.25rem',
+ 6: '1.5rem',
+ 7: '1.75rem',
+ 8: '2rem',
+ 9: '2.25rem',
+ 10: '2.5rem',
+ 11: '2.75rem',
+ 12: '3rem',
+ 14: '3.5rem',
+ 16: '4rem',
+ 20: '5rem',
+ 24: '6rem',
+ 28: '7rem',
+ 32: '8rem',
+ 36: '9rem',
+ 40: '10rem',
+ 44: '11rem',
+ 48: '12rem',
+ 52: '13rem',
+ 56: '14rem',
+ 60: '15rem',
+ 64: '16rem',
+ 72: '18rem',
+ 80: '20rem',
+ 96: '24rem',
+ },
+ strokeWidth: {
+ 0: '0',
+ 1: '1',
+ 2: '2',
+ },
+ supports: {},
+ data: {},
+ textDecorationThickness: {
+ auto: 'auto',
+ 'from-font': 'from-font',
+ 0: '0px',
+ 1: '1px',
+ 2: '2px',
+ 4: '4px',
+ 8: '8px',
+ },
+ textUnderlineOffset: {
+ auto: 'auto',
+ 0: '0px',
+ 1: '1px',
+ 2: '2px',
+ 4: '4px',
+ 8: '8px',
+ },
+ transformOrigin: {
+ center: 'center',
+ top: 'top',
+ 'top-right': 'top right',
+ right: 'right',
+ 'bottom-right': 'bottom right',
+ bottom: 'bottom',
+ 'bottom-left': 'bottom left',
+ left: 'left',
+ 'top-left': 'top left',
+ },
+ transitionDelay: {
+ 0: '0s',
+ 75: '75ms',
+ 100: '100ms',
+ 150: '150ms',
+ 200: '200ms',
+ 300: '300ms',
+ 500: '500ms',
+ 700: '700ms',
+ 1000: '1000ms',
+ },
+ transitionDuration: {
+ DEFAULT: '150ms',
+ 0: '0s',
+ 75: '75ms',
+ 100: '100ms',
+ 150: '150ms',
+ 200: '200ms',
+ 300: '300ms',
+ 500: '500ms',
+ 700: '700ms',
+ 1000: '1000ms',
+ },
+ transitionProperty: {
+ none: 'none',
+ all: 'all',
+ DEFAULT:
+ 'color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter',
+ colors: 'color, background-color, border-color, text-decoration-color, fill, stroke',
+ opacity: 'opacity',
+ shadow: 'box-shadow',
+ transform: 'transform',
+ },
+ transitionTimingFunction: {
+ DEFAULT: 'cubic-bezier(0.4, 0, 0.2, 1)',
+ linear: 'linear',
+ in: 'cubic-bezier(0.4, 0, 1, 1)',
+ out: 'cubic-bezier(0, 0, 0.2, 1)',
+ 'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
+ },
+ willChange: {
+ auto: 'auto',
+ scroll: 'scroll-position',
+ contents: 'contents',
+ transform: 'transform',
+ },
+ zIndex: {
+ auto: 'auto',
+ 0: '0',
+ 10: '10',
+ 20: '20',
+ 30: '30',
+ 40: '40',
+ 50: '50',
+ },
+}
diff --git a/packages/tailwindcss/src/plugin-api.test.ts b/packages/tailwindcss/src/plugin-api.test.ts
index c02521df0cee..c5d817a93493 100644
--- a/packages/tailwindcss/src/plugin-api.test.ts
+++ b/packages/tailwindcss/src/plugin-api.test.ts
@@ -1,6 +1,8 @@
-import { describe, test, vi } from 'vitest'
+import { describe, expect, test, vi } from 'vitest'
import { compile } from '.'
import plugin from './plugin'
+import type { CssInJs, PluginAPI } from './plugin-api'
+import { optimizeCss } from './test-utils/run'
const css = String.raw
@@ -813,3 +815,1116 @@ describe('theme', async () => {
expect(fn).toHaveBeenCalledWith({}) // Present in the resolved config
})
})
+
+describe('addUtilities()', () => {
+ test('custom static utility', async () => {
+ let compiled = await compile(
+ css`
+ @plugin "my-plugin";
+ @layer utilities {
+ @tailwind utilities;
+ }
+
+ @theme reference {
+ --breakpoint-lg: 1024px;
+ }
+ `,
+ {
+ async loadPlugin() {
+ return ({ addUtilities }: PluginAPI) => {
+ addUtilities({
+ '.text-trim': {
+ 'text-box-trim': 'both',
+ 'text-box-edge': 'cap alphabetic',
+ },
+ })
+ }
+ },
+ },
+ )
+
+ expect(optimizeCss(compiled.build(['text-trim', 'lg:text-trim'])).trim())
+ .toMatchInlineSnapshot(`
+ "@layer utilities {
+ .text-trim {
+ text-box-trim: both;
+ text-box-edge: cap alphabetic;
+ }
+
+ @media (width >= 1024px) {
+ .lg\\:text-trim {
+ text-box-trim: both;
+ text-box-edge: cap alphabetic;
+ }
+ }
+ }"
+ `)
+ })
+
+ test('return multiple rule objects from a custom utility', async () => {
+ let compiled = await compile(
+ css`
+ @plugin "my-plugin";
+ @tailwind utilities;
+ `,
+ {
+ async loadPlugin() {
+ return ({ addUtilities }: PluginAPI) => {
+ addUtilities([
+ {
+ '.text-trim': [{ 'text-box-trim': 'both' }, { 'text-box-edge': 'cap alphabetic' }],
+ },
+ ])
+ }
+ },
+ },
+ )
+
+ expect(optimizeCss(compiled.build(['text-trim'])).trim()).toMatchInlineSnapshot(`
+ ".text-trim {
+ text-box-trim: both;
+ text-box-edge: cap alphabetic;
+ }"
+ `)
+ })
+
+ test('define multiple utilities with array syntax', async () => {
+ let compiled = await compile(
+ css`
+ @plugin "my-plugin";
+ @tailwind utilities;
+ `,
+ {
+ async loadPlugin() {
+ return ({ addUtilities }: PluginAPI) => {
+ addUtilities([
+ {
+ '.text-trim': {
+ 'text-box-trim': 'both',
+ 'text-box-edge': 'cap alphabetic',
+ },
+ },
+ {
+ '.text-trim-2': {
+ 'text-box-trim': 'both',
+ 'text-box-edge': 'cap alphabetic',
+ },
+ },
+ ])
+ }
+ },
+ },
+ )
+
+ expect(optimizeCss(compiled.build(['text-trim', 'text-trim-2'])).trim()).toMatchInlineSnapshot(`
+ ".text-trim, .text-trim-2 {
+ text-box-trim: both;
+ text-box-edge: cap alphabetic;
+ }"
+ `)
+ })
+
+ test('camel case properties are converted to kebab-case', async () => {
+ let compiled = await compile(
+ css`
+ @plugin "my-plugin";
+ @layer utilities {
+ @tailwind utilities;
+ }
+ `,
+ {
+ async loadPlugin() {
+ return ({ addUtilities }: PluginAPI) => {
+ addUtilities({
+ '.text-trim': {
+ WebkitAppearance: 'none',
+ textBoxTrim: 'both',
+ textBoxEdge: 'cap alphabetic',
+ },
+ })
+ }
+ },
+ },
+ )
+
+ expect(optimizeCss(compiled.build(['text-trim'])).trim()).toMatchInlineSnapshot(`
+ "@layer utilities {
+ .text-trim {
+ -webkit-appearance: none;
+ text-box-trim: both;
+ text-box-edge: cap alphabetic;
+ }
+ }"
+ `)
+ })
+
+ test('custom static utilities support `@apply`', async () => {
+ let compiled = await compile(
+ css`
+ @plugin "my-plugin";
+ @layer utilities {
+ @tailwind utilities;
+ }
+
+ @theme reference {
+ --breakpoint-lg: 1024px;
+ }
+ `,
+ {
+ async loadPlugin() {
+ return ({ addUtilities }: PluginAPI) => {
+ addUtilities({
+ '.foo': {
+ '@apply flex dark:underline': {},
+ },
+ })
+ }
+ },
+ },
+ )
+
+ expect(optimizeCss(compiled.build(['foo', 'lg:foo'])).trim()).toMatchInlineSnapshot(`
+ "@layer utilities {
+ .foo {
+ display: flex;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ .foo {
+ text-decoration-line: underline;
+ }
+ }
+
+ @media (width >= 1024px) {
+ .lg\\:foo {
+ display: flex;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ .lg\\:foo {
+ text-decoration-line: underline;
+ }
+ }
+ }
+ }"
+ `)
+ })
+
+ test('throws on custom static utilities with an invalid name', async () => {
+ await expect(() => {
+ return compile(
+ css`
+ @plugin "my-plugin";
+ @layer utilities {
+ @tailwind utilities;
+ }
+
+ @theme reference {
+ --breakpoint-lg: 1024px;
+ }
+ `,
+ {
+ async loadPlugin() {
+ return ({ addUtilities }: PluginAPI) => {
+ addUtilities({
+ '.text-trim > *': {
+ 'text-box-trim': 'both',
+ 'text-box-edge': 'cap alphabetic',
+ },
+ })
+ }
+ },
+ },
+ )
+ }).rejects.toThrowError(/invalid utility selector/)
+ })
+
+ test('supports multiple selector names', async () => {
+ let compiled = await compile(
+ css`
+ @plugin "my-plugin";
+ @tailwind utilities;
+
+ @theme reference {
+ --breakpoint-lg: 1024px;
+ }
+ `,
+ {
+ async loadPlugin() {
+ return ({ addUtilities }: PluginAPI) => {
+ addUtilities({
+ '.form-input, .form-textarea': {
+ appearance: 'none',
+ 'background-color': '#fff',
+ },
+ })
+ }
+ },
+ },
+ )
+
+ expect(optimizeCss(compiled.build(['form-input', 'lg:form-textarea'])).trim())
+ .toMatchInlineSnapshot(`
+ ".form-input {
+ appearance: none;
+ background-color: #fff;
+ }
+
+ @media (width >= 1024px) {
+ .lg\\:form-textarea {
+ appearance: none;
+ background-color: #fff;
+ }
+ }"
+ `)
+ })
+
+ test('supports pseudo classes and pseudo elements', async () => {
+ let compiled = await compile(
+ css`
+ @plugin "my-plugin";
+ @tailwind utilities;
+
+ @theme reference {
+ --breakpoint-lg: 1024px;
+ }
+ `,
+ {
+ async loadPlugin() {
+ return ({ addUtilities }: PluginAPI) => {
+ addUtilities({
+ '.form-input, .form-input::placeholder, .form-textarea:hover:focus': {
+ 'background-color': 'red',
+ },
+ })
+ }
+ },
+ },
+ )
+
+ expect(optimizeCss(compiled.build(['form-input', 'lg:form-textarea'])).trim())
+ .toMatchInlineSnapshot(`
+ ".form-input {
+ background-color: red;
+ }
+
+ .form-input::placeholder {
+ background-color: red;
+ }
+
+ @media (width >= 1024px) {
+ .lg\\:form-textarea:hover:focus {
+ background-color: red;
+ }
+ }"
+ `)
+ })
+})
+
+describe('matchUtilities()', () => {
+ test('custom functional utility', async () => {
+ async function run(candidates: string[]) {
+ let compiled = await compile(
+ css`
+ @plugin "my-plugin";
+
+ @tailwind utilities;
+
+ @theme reference {
+ --breakpoint-lg: 1024px;
+ }
+ `,
+ {
+ async loadPlugin() {
+ return ({ matchUtilities }: PluginAPI) => {
+ matchUtilities(
+ {
+ 'border-block': (value) => ({ 'border-block-width': value }),
+ },
+ {
+ values: {
+ DEFAULT: '1px',
+ '2': '2px',
+ },
+ },
+ )
+ }
+ },
+ },
+ )
+
+ return compiled.build(candidates)
+ }
+
+ expect(
+ optimizeCss(
+ await run([
+ 'border-block',
+ 'border-block-2',
+ 'border-block-[35px]',
+ 'border-block-[var(--foo)]',
+ 'lg:border-block-2',
+ ]),
+ ).trim(),
+ ).toMatchInlineSnapshot(`
+ ".border-block {
+ border-block-width: 1px;
+ }
+
+ .border-block-2 {
+ border-block-width: 2px;
+ }
+
+ .border-block-\\[35px\\] {
+ border-block-width: 35px;
+ }
+
+ .border-block-\\[var\\(--foo\\)\\] {
+ border-block-width: var(--foo);
+ }
+
+ @media (width >= 1024px) {
+ .lg\\:border-block-2 {
+ border-block-width: 2px;
+ }
+ }"
+ `)
+
+ expect(
+ optimizeCss(
+ await run([
+ '-border-block',
+ '-border-block-2',
+ 'lg:-border-block-2',
+ 'border-block-unknown',
+ 'border-block/1',
+ ]),
+ ).trim(),
+ ).toEqual('')
+ })
+
+ test('custom functional utilities can return an array of rules', async () => {
+ let compiled = await compile(
+ css`
+ @plugin "my-plugin";
+ @tailwind utilities;
+ `,
+ {
+ async loadPlugin() {
+ return ({ matchUtilities }: PluginAPI) => {
+ matchUtilities(
+ {
+ 'all-but-order-bottom-left-radius': (value) =>
+ [
+ { 'border-top-left-radius': value },
+ { 'border-top-right-radius': value },
+ { 'border-bottom-right-radius': value },
+ ] as CssInJs[],
+ },
+ {
+ values: {
+ DEFAULT: '1px',
+ },
+ },
+ )
+ }
+ },
+ },
+ )
+
+ expect(optimizeCss(compiled.build(['all-but-order-bottom-left-radius'])).trim())
+ .toMatchInlineSnapshot(`
+ ".all-but-order-bottom-left-radius {
+ border-top-left-radius: 1px;
+ border-top-right-radius: 1px;
+ border-bottom-right-radius: 1px;
+ }"
+ `)
+ })
+
+ test('custom functional utility with any modifier', async () => {
+ async function run(candidates: string[]) {
+ let compiled = await compile(
+ css`
+ @plugin "my-plugin";
+
+ @tailwind utilities;
+
+ @theme reference {
+ --breakpoint-lg: 1024px;
+ }
+ `,
+ {
+ async loadPlugin() {
+ return ({ matchUtilities }: PluginAPI) => {
+ matchUtilities(
+ {
+ 'border-block': (value, { modifier }) => ({
+ '--my-modifier': modifier ?? 'none',
+ 'border-block-width': value,
+ }),
+ },
+ {
+ values: {
+ DEFAULT: '1px',
+ '2': '2px',
+ },
+
+ modifiers: 'any',
+ },
+ )
+ }
+ },
+ },
+ )
+
+ return compiled.build(candidates)
+ }
+
+ expect(
+ optimizeCss(
+ await run(['border-block', 'border-block-2', 'border-block/foo', 'border-block-2/foo']),
+ ).trim(),
+ ).toMatchInlineSnapshot(`
+ ".border-block {
+ --my-modifier: none;
+ border-block-width: 1px;
+ }
+
+ .border-block-2 {
+ --my-modifier: none;
+ border-block-width: 2px;
+ }
+
+ .border-block-2\\/foo {
+ --my-modifier: foo;
+ border-block-width: 2px;
+ }
+
+ .border-block\\/foo {
+ --my-modifier: foo;
+ border-block-width: 1px;
+ }"
+ `)
+ })
+
+ test('custom functional utility with known modifier', async () => {
+ async function run(candidates: string[]) {
+ let compiled = await compile(
+ css`
+ @plugin "my-plugin";
+
+ @tailwind utilities;
+
+ @theme reference {
+ --breakpoint-lg: 1024px;
+ }
+ `,
+ {
+ async loadPlugin() {
+ return ({ matchUtilities }: PluginAPI) => {
+ matchUtilities(
+ {
+ 'border-block': (value, { modifier }) => ({
+ '--my-modifier': modifier ?? 'none',
+ 'border-block-width': value,
+ }),
+ },
+ {
+ values: {
+ DEFAULT: '1px',
+ '2': '2px',
+ },
+
+ modifiers: {
+ foo: 'foo',
+ },
+ },
+ )
+ }
+ },
+ },
+ )
+
+ return compiled.build(candidates)
+ }
+
+ expect(
+ optimizeCss(
+ await run(['border-block', 'border-block-2', 'border-block/foo', 'border-block-2/foo']),
+ ).trim(),
+ ).toMatchInlineSnapshot(`
+ ".border-block {
+ --my-modifier: none;
+ border-block-width: 1px;
+ }
+
+ .border-block-2 {
+ --my-modifier: none;
+ border-block-width: 2px;
+ }
+
+ .border-block-2\\/foo {
+ --my-modifier: foo;
+ border-block-width: 2px;
+ }
+
+ .border-block\\/foo {
+ --my-modifier: foo;
+ border-block-width: 1px;
+ }"
+ `)
+
+ expect(
+ optimizeCss(await run(['border-block/unknown', 'border-block-2/unknown'])).trim(),
+ ).toEqual('')
+ })
+
+ // We're not married to this behavior — if there's a good reason to do this differently in the
+ // future don't be afraid to change what should happen in this scenario.
+ describe('plugins that handle a specific arbitrary value type prevent falling through to other plugins if the result is invalid for that plugin', () => {
+ test('implicit color modifier', async () => {
+ async function run(candidates: string[]) {
+ let compiled = await compile(
+ css`
+ @tailwind utilities;
+ @plugin "my-plugin";
+ `,
+ {
+ async loadPlugin() {
+ return ({ matchUtilities }: PluginAPI) => {
+ matchUtilities(
+ {
+ scrollbar: (value) => ({ 'scrollbar-color': value }),
+ },
+ { type: ['color', 'any'] },
+ )
+
+ matchUtilities(
+ {
+ scrollbar: (value) => ({ 'scrollbar-width': value }),
+ },
+ { type: ['length'] },
+ )
+ }
+ },
+ },
+ )
+
+ return compiled.build(candidates)
+ }
+
+ expect(
+ optimizeCss(
+ await run(['scrollbar-[2px]', 'scrollbar-[#08c]', 'scrollbar-[#08c]/50']),
+ ).trim(),
+ ).toMatchInlineSnapshot(`
+ ".scrollbar-\\[\\#08c\\] {
+ scrollbar-color: #08c;
+ }
+
+ .scrollbar-\\[\\#08c\\]\\/50 {
+ scrollbar-color: #0088cc80;
+ }
+
+ .scrollbar-\\[2px\\] {
+ scrollbar-width: 2px;
+ }"
+ `)
+ expect(optimizeCss(await run(['scrollbar-[2px]/50'])).trim()).toEqual('')
+ })
+
+ test('no modifiers are supported by the plugins', async () => {
+ async function run(candidates: string[]) {
+ let compiled = await compile(
+ css`
+ @tailwind utilities;
+ @plugin "my-plugin";
+ `,
+ {
+ async loadPlugin() {
+ return ({ matchUtilities }: PluginAPI) => {
+ matchUtilities(
+ {
+ scrollbar: (value) => ({ '--scrollbar-angle': value }),
+ },
+ { type: ['angle', 'any'] },
+ )
+
+ matchUtilities(
+ {
+ scrollbar: (value) => ({ '--scrollbar-width': value }),
+ },
+ { type: ['length'] },
+ )
+ }
+ },
+ },
+ )
+
+ return compiled.build(candidates)
+ }
+
+ expect(optimizeCss(await run(['scrollbar-[2px]/50'])).trim()).toEqual('')
+ })
+
+ test('invalid named modifier', async () => {
+ async function run(candidates: string[]) {
+ let compiled = await compile(
+ css`
+ @tailwind utilities;
+ @plugin "my-plugin";
+ `,
+ {
+ async loadPlugin() {
+ return ({ matchUtilities }: PluginAPI) => {
+ matchUtilities(
+ {
+ scrollbar: (value) => ({ 'scrollbar-color': value }),
+ },
+ { type: ['color', 'any'], modifiers: { foo: 'foo' } },
+ )
+
+ matchUtilities(
+ {
+ scrollbar: (value) => ({ 'scrollbar-width': value }),
+ },
+ { type: ['length'], modifiers: { bar: 'bar' } },
+ )
+ }
+ },
+ },
+ )
+
+ return compiled.build(candidates)
+ }
+
+ expect(optimizeCss(await run(['scrollbar-[2px]/foo'])).trim()).toEqual('')
+ })
+ })
+
+ test('custom functional utilities with different types', async () => {
+ async function run(candidates: string[]) {
+ let compiled = await compile(
+ css`
+ @plugin "my-plugin";
+
+ @tailwind utilities;
+
+ @theme reference {
+ --breakpoint-lg: 1024px;
+ }
+ `,
+ {
+ async loadPlugin() {
+ return ({ matchUtilities }: PluginAPI) => {
+ matchUtilities(
+ {
+ scrollbar: (value) => ({ 'scrollbar-color': value }),
+ },
+ {
+ type: ['color', 'any'],
+ values: {
+ black: 'black',
+ },
+ },
+ )
+
+ matchUtilities(
+ {
+ scrollbar: (value) => ({ 'scrollbar-width': value }),
+ },
+ {
+ type: ['length'],
+ values: {
+ 2: '2px',
+ },
+ },
+ )
+ }
+ },
+ },
+ )
+
+ return compiled.build(candidates)
+ }
+
+ expect(
+ optimizeCss(
+ await run([
+ 'scrollbar-black',
+ 'scrollbar-black/50',
+ 'scrollbar-2',
+ 'scrollbar-[#fff]',
+ 'scrollbar-[#fff]/50',
+ 'scrollbar-[2px]',
+ 'scrollbar-[var(--my-color)]',
+ 'scrollbar-[var(--my-color)]/50',
+ 'scrollbar-[color:var(--my-color)]',
+ 'scrollbar-[color:var(--my-color)]/50',
+ 'scrollbar-[length:var(--my-width)]',
+ ]),
+ ).trim(),
+ ).toMatchInlineSnapshot(`
+ ".scrollbar-2 {
+ scrollbar-width: 2px;
+ }
+
+ .scrollbar-\\[\\#fff\\] {
+ scrollbar-color: #fff;
+ }
+
+ .scrollbar-\\[\\#fff\\]\\/50 {
+ scrollbar-color: #ffffff80;
+ }
+
+ .scrollbar-\\[2px\\] {
+ scrollbar-width: 2px;
+ }
+
+ .scrollbar-\\[color\\:var\\(--my-color\\)\\] {
+ scrollbar-color: var(--my-color);
+ }
+
+ .scrollbar-\\[color\\:var\\(--my-color\\)\\]\\/50 {
+ scrollbar-color: color-mix(in srgb, var(--my-color) 50%, transparent);
+ }
+
+ .scrollbar-\\[length\\:var\\(--my-width\\)\\] {
+ scrollbar-width: var(--my-width);
+ }
+
+ .scrollbar-\\[var\\(--my-color\\)\\] {
+ scrollbar-color: var(--my-color);
+ }
+
+ .scrollbar-\\[var\\(--my-color\\)\\]\\/50 {
+ scrollbar-color: color-mix(in srgb, var(--my-color) 50%, transparent);
+ }
+
+ .scrollbar-black {
+ scrollbar-color: black;
+ }
+
+ .scrollbar-black\\/50 {
+ scrollbar-color: #00000080;
+ }"
+ `)
+
+ expect(
+ optimizeCss(
+ await run([
+ 'scrollbar-2/50',
+ 'scrollbar-[2px]/50',
+ 'scrollbar-[length:var(--my-width)]/50',
+ ]),
+ ).trim(),
+ ).toEqual('')
+ })
+
+ test('functional utilities with `type: color` automatically support opacity', async () => {
+ async function run(candidates: string[]) {
+ let compiled = await compile(
+ css`
+ @plugin "my-plugin";
+
+ @tailwind utilities;
+
+ @theme reference {
+ --breakpoint-lg: 1024px;
+ }
+ `,
+ {
+ async loadPlugin() {
+ return ({ matchUtilities }: PluginAPI) => {
+ matchUtilities(
+ {
+ scrollbar: (value) => ({ 'scrollbar-color': value }),
+ },
+ {
+ type: ['color', 'any'],
+ values: {
+ black: 'black',
+ },
+ },
+ )
+ }
+ },
+ },
+ )
+
+ return compiled.build(candidates)
+ }
+
+ expect(
+ optimizeCss(
+ await run([
+ 'scrollbar-current',
+ 'scrollbar-current/45',
+ 'scrollbar-black',
+ 'scrollbar-black/33',
+ 'scrollbar-black/[50%]',
+ 'scrollbar-[var(--my-color)]/[25%]',
+ ]),
+ ).trim(),
+ ).toMatchInlineSnapshot(`
+ ".scrollbar-\\[var\\(--my-color\\)\\]\\/\\[25\\%\\] {
+ scrollbar-color: color-mix(in srgb, var(--my-color) 25%, transparent);
+ }
+
+ .scrollbar-black {
+ scrollbar-color: black;
+ }
+
+ .scrollbar-black\\/33 {
+ scrollbar-color: #00000054;
+ }
+
+ .scrollbar-black\\/\\[50\\%\\] {
+ scrollbar-color: #00000080;
+ }
+
+ .scrollbar-current {
+ scrollbar-color: currentColor;
+ }
+
+ .scrollbar-current\\/45 {
+ scrollbar-color: color-mix(in srgb, currentColor 45%, transparent);
+ }"
+ `)
+ })
+
+ test('functional utilities with explicit modifiers', async () => {
+ async function run(candidates: string[]) {
+ let compiled = await compile(
+ css`
+ @plugin "my-plugin";
+
+ @tailwind utilities;
+
+ @theme reference {
+ --breakpoint-lg: 1024px;
+ --opacity-my-opacity: 0.5;
+ }
+ `,
+ {
+ async loadPlugin() {
+ return ({ matchUtilities }: PluginAPI) => {
+ matchUtilities(
+ {
+ scrollbar: (value, { modifier }) => ({
+ '--modifier': modifier ?? 'none',
+ 'scrollbar-width': value,
+ }),
+ },
+ {
+ type: ['any'],
+ values: {},
+ modifiers: {
+ foo: 'foo',
+ },
+ },
+ )
+ }
+ },
+ },
+ )
+
+ return compiled.build(candidates)
+ }
+
+ expect(
+ optimizeCss(
+ await run(['scrollbar-[12px]', 'scrollbar-[12px]/foo', 'scrollbar-[12px]/bar']),
+ ).trim(),
+ ).toMatchInlineSnapshot(`
+ ".scrollbar-\\[12px\\] {
+ --modifier: none;
+ scrollbar-width: 12px;
+ }
+
+ .scrollbar-\\[12px\\]\\/foo {
+ --modifier: foo;
+ scrollbar-width: 12px;
+ }"
+ `)
+ })
+
+ test('functional utilities support `@apply`', async () => {
+ let compiled = await compile(
+ css`
+ @plugin "my-plugin";
+ @layer utilities {
+ @tailwind utilities;
+ }
+
+ @theme reference {
+ --breakpoint-lg: 1024px;
+ }
+ `,
+ {
+ async loadPlugin() {
+ return ({ matchUtilities }: PluginAPI) => {
+ matchUtilities(
+ {
+ foo: (value) => ({
+ '--foo': value,
+ [`@apply flex`]: {},
+ }),
+ },
+ {
+ values: {
+ bar: 'bar',
+ },
+ },
+ )
+ }
+ },
+ },
+ )
+
+ expect(
+ optimizeCss(compiled.build(['foo-bar', 'lg:foo-bar', 'foo-[12px]', 'lg:foo-[12px]'])).trim(),
+ ).toMatchInlineSnapshot(`
+ "@layer utilities {
+ .foo-\\[12px\\] {
+ --foo: 12px;
+ display: flex;
+ }
+
+ .foo-bar {
+ --foo: bar;
+ display: flex;
+ }
+
+ @media (width >= 1024px) {
+ .lg\\:foo-\\[12px\\] {
+ --foo: 12px;
+ display: flex;
+ }
+ }
+
+ @media (width >= 1024px) {
+ .lg\\:foo-bar {
+ --foo: bar;
+ display: flex;
+ }
+ }
+ }"
+ `)
+ })
+
+ test('throws on custom utilities with an invalid name', async () => {
+ await expect(() => {
+ return compile(
+ css`
+ @plugin "my-plugin";
+ @layer utilities {
+ @tailwind utilities;
+ }
+
+ @theme reference {
+ --breakpoint-lg: 1024px;
+ }
+ `,
+ {
+ async loadPlugin() {
+ return ({ matchUtilities }: PluginAPI) => {
+ matchUtilities({
+ '.text-trim > *': () => ({
+ 'text-box-trim': 'both',
+ 'text-box-edge': 'cap alphabetic',
+ }),
+ })
+ }
+ },
+ },
+ )
+ }).rejects.toThrowError(/invalid utility name/)
+ })
+})
+
+describe('addComponents()', () => {
+ test('is an alias for addUtilities', async () => {
+ let compiled = await compile(
+ css`
+ @plugin "my-plugin";
+ @tailwind utilities;
+ `,
+ {
+ async loadPlugin() {
+ return ({ addComponents }: PluginAPI) => {
+ addComponents({
+ '.btn': {
+ padding: '.5rem 1rem',
+ borderRadius: '.25rem',
+ fontWeight: '600',
+ },
+ '.btn-blue': {
+ backgroundColor: '#3490dc',
+ color: '#fff',
+ '&:hover': {
+ backgroundColor: '#2779bd',
+ },
+ },
+ '.btn-red': {
+ backgroundColor: '#e3342f',
+ color: '#fff',
+ '&:hover': {
+ backgroundColor: '#cc1f1a',
+ },
+ },
+ })
+ }
+ },
+ },
+ )
+
+ expect(optimizeCss(compiled.build(['btn', 'btn-blue', 'btn-red'])).trim())
+ .toMatchInlineSnapshot(`
+ ".btn {
+ border-radius: .25rem;
+ padding: .5rem 1rem;
+ font-weight: 600;
+ }
+
+ .btn-blue {
+ color: #fff;
+ background-color: #3490dc;
+ }
+
+ .btn-blue:hover {
+ background-color: #2779bd;
+ }
+
+ .btn-red {
+ color: #fff;
+ background-color: #e3342f;
+ }
+
+ .btn-red:hover {
+ background-color: #cc1f1a;
+ }"
+ `)
+ })
+})
+
+describe('prefix()', () => {
+ test('is an identity function', async () => {
+ let fn = vi.fn()
+ await compile(
+ css`
+ @plugin "my-plugin";
+ `,
+ {
+ async loadPlugin() {
+ return ({ prefix }: PluginAPI) => {
+ fn(prefix('btn'))
+ }
+ },
+ },
+ )
+
+ expect(fn).toHaveBeenCalledWith('btn')
+ })
+})
diff --git a/packages/tailwindcss/src/plugin-api.ts b/packages/tailwindcss/src/plugin-api.ts
index 9140a777260c..89666988776f 100644
--- a/packages/tailwindcss/src/plugin-api.ts
+++ b/packages/tailwindcss/src/plugin-api.ts
@@ -8,6 +8,7 @@ import type { DesignSystem } from './design-system'
import { createThemeFn } from './theme-fn'
import { withAlpha, withNegative } from './utilities'
import { inferDataType } from './utils/infer-data-type'
+import { segment } from './utils/segment'
export type Config = UserConfig
export type PluginFn = (api: PluginAPI) => void
@@ -22,8 +23,28 @@ export type Plugin = PluginFn | PluginWithConfig | PluginWithOptions
export type PluginAPI = {
addBase(base: CssInJs): void
addVariant(name: string, variant: string | string[] | CssInJs): void
- addUtilities(utilities: Record, options?: {}): void
+
+ addUtilities(
+ utilities: Record | Record[],
+ options?: {},
+ ): void
matchUtilities(
+ utilities: Record<
+ string,
+ (value: string, extra: { modifier: string | null }) => CssInJs | CssInJs[]
+ >,
+ options?: Partial<{
+ type: string | string[]
+ supportsNegativeValues: boolean
+ values: Record & {
+ __BARE_VALUE__?: (value: NamedUtilityValue) => string | undefined
+ }
+ modifiers: 'any' | Record
+ }>,
+ ): void
+
+ addComponents(utilities: Record | Record[], options?: {}): void
+ matchComponents(
utilities: Record CssInJs>,
options?: Partial<{
type: string | string[]
@@ -34,7 +55,9 @@ export type PluginAPI = {
modifiers: 'any' | Record
}>,
): void
+
theme(path: string, defaultValue?: any): any
+ prefix(className: string): string
}
const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/
@@ -44,7 +67,7 @@ function buildPluginApi(
ast: AstNode[],
resolvedConfig: { theme?: Record },
): PluginAPI {
- return {
+ let api: PluginAPI = {
addBase(css) {
ast.push(rule('@layer base', objectToAst(css)))
},
@@ -71,7 +94,35 @@ function buildPluginApi(
},
addUtilities(utilities) {
- for (let [name, css] of Object.entries(utilities)) {
+ utilities = Array.isArray(utilities) ? utilities : [utilities]
+
+ let entries = utilities.flatMap((u) => Object.entries(u))
+
+ // Split multi-selector utilities into individual utilities
+ entries = entries.flatMap(([name, css]) =>
+ segment(name, ',').map((selector) => [selector.trim(), css] as [string, CssInJs]),
+ )
+
+ // Merge entries for the same class
+ let utils: Record = {}
+
+ for (let [name, css] of entries) {
+ let [className, ...parts] = segment(name, ':')
+
+ // Modify classes using pseudo-classes or pseudo-elements to use nested rules
+ if (parts.length > 0) {
+ let pseudos = parts.map((p) => `:${p.trim()}`).join('')
+ css = {
+ [`&${pseudos}`]: css,
+ }
+ }
+
+ utils[className] ??= []
+ css = Array.isArray(css) ? css : [css]
+ utils[className].push(...css)
+ }
+
+ for (let [name, css] of Object.entries(utils)) {
if (name.startsWith('@keyframes ')) {
ast.push(rule(name, objectToAst(css)))
continue
@@ -85,7 +136,6 @@ function buildPluginApi(
designSystem.utilities.static(name.slice(1), (candidate) => {
if (candidate.negative) return
-
let ast = objectToAst(css)
substituteAtApply(ast, designSystem)
return ast
@@ -210,20 +260,42 @@ function buildPluginApi(
}
},
+ addComponents(components, options) {
+ this.addUtilities(components)
+ },
+
+ matchComponents(components, options) {
+ this.matchUtilities(components)
+ },
+
theme: createThemeFn(
designSystem,
() => resolvedConfig.theme ?? {},
(value) => value,
),
+
+ prefix(className) {
+ return className
+ },
}
+
+ // Bind these functions so they can use `this`
+ api.addComponents = api.addComponents.bind(api)
+ api.matchComponents = api.matchComponents.bind(api)
+
+ return api
}
-export type CssInJs = { [key: string]: string | CssInJs }
+export type CssInJs = { [key: string]: string | CssInJs | CssInJs[] }
-function objectToAst(obj: CssInJs): AstNode[] {
+function objectToAst(rules: CssInJs | CssInJs[]): AstNode[] {
let ast: AstNode[] = []
- for (let [name, value] of Object.entries(obj)) {
+ rules = Array.isArray(rules) ? rules : [rules]
+
+ let entries = rules.flatMap((rule) => Object.entries(rule))
+
+ for (let [name, value] of entries) {
if (typeof value !== 'object') {
if (!name.startsWith('--') && value === '@slot') {
ast.push(rule(name, [rule('@slot', [])]))
diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts
index a01e8473f048..0d04f7caa4d2 100644
--- a/packages/tailwindcss/src/utilities.test.ts
+++ b/packages/tailwindcss/src/utilities.test.ts
@@ -1,6 +1,5 @@
import { describe, expect, test } from 'vitest'
import { compile } from '.'
-import type { PluginAPI } from './plugin-api'
import { compileCss, optimizeCss, run } from './test-utils/run'
const css = String.raw
@@ -15396,848 +15395,3 @@ describe('custom utilities', () => {
)
})
})
-
-describe('legacy: addUtilities', () => {
- test('custom static utility', async () => {
- let compiled = await compile(
- css`
- @plugin "my-plugin";
- @layer utilities {
- @tailwind utilities;
- }
-
- @theme reference {
- --breakpoint-lg: 1024px;
- }
- `,
- {
- async loadPlugin() {
- return ({ addUtilities }: PluginAPI) => {
- addUtilities({
- '.text-trim': {
- 'text-box-trim': 'both',
- 'text-box-edge': 'cap alphabetic',
- },
- })
- }
- },
- },
- )
-
- expect(optimizeCss(compiled.build(['text-trim', 'lg:text-trim'])).trim())
- .toMatchInlineSnapshot(`
- "@layer utilities {
- .text-trim {
- text-box-trim: both;
- text-box-edge: cap alphabetic;
- }
-
- @media (width >= 1024px) {
- .lg\\:text-trim {
- text-box-trim: both;
- text-box-edge: cap alphabetic;
- }
- }
- }"
- `)
- })
-
- test('camel case properties are converted to kebab-case', async () => {
- let compiled = await compile(
- css`
- @plugin "my-plugin";
- @layer utilities {
- @tailwind utilities;
- }
- `,
- {
- async loadPlugin() {
- return ({ addUtilities }: PluginAPI) => {
- addUtilities({
- '.text-trim': {
- WebkitAppearance: 'none',
- textBoxTrim: 'both',
- textBoxEdge: 'cap alphabetic',
- },
- })
- }
- },
- },
- )
-
- expect(optimizeCss(compiled.build(['text-trim'])).trim()).toMatchInlineSnapshot(`
- "@layer utilities {
- .text-trim {
- -webkit-appearance: none;
- text-box-trim: both;
- text-box-edge: cap alphabetic;
- }
- }"
- `)
- })
-
- test('custom static utilities support `@apply`', async () => {
- let compiled = await compile(
- css`
- @plugin "my-plugin";
- @layer utilities {
- @tailwind utilities;
- }
-
- @theme reference {
- --breakpoint-lg: 1024px;
- }
- `,
- {
- async loadPlugin() {
- return ({ addUtilities }: PluginAPI) => {
- addUtilities({
- '.foo': {
- '@apply flex dark:underline': {},
- },
- })
- }
- },
- },
- )
-
- expect(optimizeCss(compiled.build(['foo', 'lg:foo'])).trim()).toMatchInlineSnapshot(`
- "@layer utilities {
- .foo {
- display: flex;
- }
-
- @media (prefers-color-scheme: dark) {
- .foo {
- text-decoration-line: underline;
- }
- }
-
- @media (width >= 1024px) {
- .lg\\:foo {
- display: flex;
- }
-
- @media (prefers-color-scheme: dark) {
- .lg\\:foo {
- text-decoration-line: underline;
- }
- }
- }
- }"
- `)
- })
-
- test('throws on custom static utilities with an invalid name', async () => {
- await expect(() => {
- return compile(
- css`
- @plugin "my-plugin";
- @layer utilities {
- @tailwind utilities;
- }
-
- @theme reference {
- --breakpoint-lg: 1024px;
- }
- `,
- {
- async loadPlugin() {
- return ({ addUtilities }: PluginAPI) => {
- addUtilities({
- '.text-trim > *': {
- 'text-box-trim': 'both',
- 'text-box-edge': 'cap alphabetic',
- },
- })
- }
- },
- },
- )
- }).rejects.toThrowError(/invalid utility selector/)
- })
-})
-
-describe('legacy: matchUtilities', () => {
- test('custom functional utility', async () => {
- async function run(candidates: string[]) {
- let compiled = await compile(
- css`
- @plugin "my-plugin";
-
- @tailwind utilities;
-
- @theme reference {
- --breakpoint-lg: 1024px;
- }
- `,
- {
- async loadPlugin() {
- return ({ matchUtilities }: PluginAPI) => {
- matchUtilities(
- {
- 'border-block': (value) => ({ 'border-block-width': value }),
- },
- {
- values: {
- DEFAULT: '1px',
- '2': '2px',
- },
- },
- )
- }
- },
- },
- )
-
- return compiled.build(candidates)
- }
-
- expect(
- optimizeCss(
- await run([
- 'border-block',
- 'border-block-2',
- 'border-block-[35px]',
- 'border-block-[var(--foo)]',
- 'lg:border-block-2',
- ]),
- ).trim(),
- ).toMatchInlineSnapshot(`
- ".border-block {
- border-block-width: 1px;
- }
-
- .border-block-2 {
- border-block-width: 2px;
- }
-
- .border-block-\\[35px\\] {
- border-block-width: 35px;
- }
-
- .border-block-\\[var\\(--foo\\)\\] {
- border-block-width: var(--foo);
- }
-
- @media (width >= 1024px) {
- .lg\\:border-block-2 {
- border-block-width: 2px;
- }
- }"
- `)
-
- expect(
- optimizeCss(
- await run([
- '-border-block',
- '-border-block-2',
- 'lg:-border-block-2',
- 'border-block-unknown',
- 'border-block/1',
- ]),
- ).trim(),
- ).toEqual('')
- })
-
- test('custom functional utility with any modifier', async () => {
- async function run(candidates: string[]) {
- let compiled = await compile(
- css`
- @plugin "my-plugin";
-
- @tailwind utilities;
-
- @theme reference {
- --breakpoint-lg: 1024px;
- }
- `,
- {
- async loadPlugin() {
- return ({ matchUtilities }: PluginAPI) => {
- matchUtilities(
- {
- 'border-block': (value, { modifier }) => ({
- '--my-modifier': modifier ?? 'none',
- 'border-block-width': value,
- }),
- },
- {
- values: {
- DEFAULT: '1px',
- '2': '2px',
- },
-
- modifiers: 'any',
- },
- )
- }
- },
- },
- )
-
- return compiled.build(candidates)
- }
-
- expect(
- optimizeCss(
- await run(['border-block', 'border-block-2', 'border-block/foo', 'border-block-2/foo']),
- ).trim(),
- ).toMatchInlineSnapshot(`
- ".border-block {
- --my-modifier: none;
- border-block-width: 1px;
- }
-
- .border-block-2 {
- --my-modifier: none;
- border-block-width: 2px;
- }
-
- .border-block-2\\/foo {
- --my-modifier: foo;
- border-block-width: 2px;
- }
-
- .border-block\\/foo {
- --my-modifier: foo;
- border-block-width: 1px;
- }"
- `)
- })
-
- test('custom functional utility with known modifier', async () => {
- async function run(candidates: string[]) {
- let compiled = await compile(
- css`
- @plugin "my-plugin";
-
- @tailwind utilities;
-
- @theme reference {
- --breakpoint-lg: 1024px;
- }
- `,
- {
- async loadPlugin() {
- return ({ matchUtilities }: PluginAPI) => {
- matchUtilities(
- {
- 'border-block': (value, { modifier }) => ({
- '--my-modifier': modifier ?? 'none',
- 'border-block-width': value,
- }),
- },
- {
- values: {
- DEFAULT: '1px',
- '2': '2px',
- },
-
- modifiers: {
- foo: 'foo',
- },
- },
- )
- }
- },
- },
- )
-
- return compiled.build(candidates)
- }
-
- expect(
- optimizeCss(
- await run(['border-block', 'border-block-2', 'border-block/foo', 'border-block-2/foo']),
- ).trim(),
- ).toMatchInlineSnapshot(`
- ".border-block {
- --my-modifier: none;
- border-block-width: 1px;
- }
-
- .border-block-2 {
- --my-modifier: none;
- border-block-width: 2px;
- }
-
- .border-block-2\\/foo {
- --my-modifier: foo;
- border-block-width: 2px;
- }
-
- .border-block\\/foo {
- --my-modifier: foo;
- border-block-width: 1px;
- }"
- `)
-
- expect(
- optimizeCss(await run(['border-block/unknown', 'border-block-2/unknown'])).trim(),
- ).toEqual('')
- })
-
- // We're not married to this behavior — if there's a good reason to do this differently in the
- // future don't be afraid to change what should happen in this scenario.
- describe('plugins that handle a specific arbitrary value type prevent falling through to other plugins if the result is invalid for that plugin', () => {
- test('implicit color modifier', async () => {
- async function run(candidates: string[]) {
- let compiled = await compile(
- css`
- @tailwind utilities;
- @plugin "my-plugin";
- `,
- {
- async loadPlugin() {
- return ({ matchUtilities }: PluginAPI) => {
- matchUtilities(
- {
- scrollbar: (value) => ({ 'scrollbar-color': value }),
- },
- { type: ['color', 'any'] },
- )
-
- matchUtilities(
- {
- scrollbar: (value) => ({ 'scrollbar-width': value }),
- },
- { type: ['length'] },
- )
- }
- },
- },
- )
-
- return compiled.build(candidates)
- }
-
- expect(
- optimizeCss(
- await run(['scrollbar-[2px]', 'scrollbar-[#08c]', 'scrollbar-[#08c]/50']),
- ).trim(),
- ).toMatchInlineSnapshot(`
- ".scrollbar-\\[\\#08c\\] {
- scrollbar-color: #08c;
- }
-
- .scrollbar-\\[\\#08c\\]\\/50 {
- scrollbar-color: #0088cc80;
- }
-
- .scrollbar-\\[2px\\] {
- scrollbar-width: 2px;
- }"
- `)
- expect(optimizeCss(await run(['scrollbar-[2px]/50'])).trim()).toEqual('')
- })
-
- test('no modifiers are supported by the plugins', async () => {
- async function run(candidates: string[]) {
- let compiled = await compile(
- css`
- @tailwind utilities;
- @plugin "my-plugin";
- `,
- {
- async loadPlugin() {
- return ({ matchUtilities }: PluginAPI) => {
- matchUtilities(
- {
- scrollbar: (value) => ({ '--scrollbar-angle': value }),
- },
- { type: ['angle', 'any'] },
- )
-
- matchUtilities(
- {
- scrollbar: (value) => ({ '--scrollbar-width': value }),
- },
- { type: ['length'] },
- )
- }
- },
- },
- )
-
- return compiled.build(candidates)
- }
-
- expect(optimizeCss(await run(['scrollbar-[2px]/50'])).trim()).toEqual('')
- })
-
- test('invalid named modifier', async () => {
- async function run(candidates: string[]) {
- let compiled = await compile(
- css`
- @tailwind utilities;
- @plugin "my-plugin";
- `,
- {
- async loadPlugin() {
- return ({ matchUtilities }: PluginAPI) => {
- matchUtilities(
- {
- scrollbar: (value) => ({ 'scrollbar-color': value }),
- },
- { type: ['color', 'any'], modifiers: { foo: 'foo' } },
- )
-
- matchUtilities(
- {
- scrollbar: (value) => ({ 'scrollbar-width': value }),
- },
- { type: ['length'], modifiers: { bar: 'bar' } },
- )
- }
- },
- },
- )
-
- return compiled.build(candidates)
- }
-
- expect(optimizeCss(await run(['scrollbar-[2px]/foo'])).trim()).toEqual('')
- })
- })
-
- test('custom functional utilities with different types', async () => {
- async function run(candidates: string[]) {
- let compiled = await compile(
- css`
- @plugin "my-plugin";
-
- @tailwind utilities;
-
- @theme reference {
- --breakpoint-lg: 1024px;
- }
- `,
- {
- async loadPlugin() {
- return ({ matchUtilities }: PluginAPI) => {
- matchUtilities(
- {
- scrollbar: (value) => ({ 'scrollbar-color': value }),
- },
- {
- type: ['color', 'any'],
- values: {
- black: 'black',
- },
- },
- )
-
- matchUtilities(
- {
- scrollbar: (value) => ({ 'scrollbar-width': value }),
- },
- {
- type: ['length'],
- values: {
- 2: '2px',
- },
- },
- )
- }
- },
- },
- )
-
- return compiled.build(candidates)
- }
-
- expect(
- optimizeCss(
- await run([
- 'scrollbar-black',
- 'scrollbar-black/50',
- 'scrollbar-2',
- 'scrollbar-[#fff]',
- 'scrollbar-[#fff]/50',
- 'scrollbar-[2px]',
- 'scrollbar-[var(--my-color)]',
- 'scrollbar-[var(--my-color)]/50',
- 'scrollbar-[color:var(--my-color)]',
- 'scrollbar-[color:var(--my-color)]/50',
- 'scrollbar-[length:var(--my-width)]',
- ]),
- ).trim(),
- ).toMatchInlineSnapshot(`
- ".scrollbar-2 {
- scrollbar-width: 2px;
- }
-
- .scrollbar-\\[\\#fff\\] {
- scrollbar-color: #fff;
- }
-
- .scrollbar-\\[\\#fff\\]\\/50 {
- scrollbar-color: #ffffff80;
- }
-
- .scrollbar-\\[2px\\] {
- scrollbar-width: 2px;
- }
-
- .scrollbar-\\[color\\:var\\(--my-color\\)\\] {
- scrollbar-color: var(--my-color);
- }
-
- .scrollbar-\\[color\\:var\\(--my-color\\)\\]\\/50 {
- scrollbar-color: color-mix(in srgb, var(--my-color) 50%, transparent);
- }
-
- .scrollbar-\\[length\\:var\\(--my-width\\)\\] {
- scrollbar-width: var(--my-width);
- }
-
- .scrollbar-\\[var\\(--my-color\\)\\] {
- scrollbar-color: var(--my-color);
- }
-
- .scrollbar-\\[var\\(--my-color\\)\\]\\/50 {
- scrollbar-color: color-mix(in srgb, var(--my-color) 50%, transparent);
- }
-
- .scrollbar-black {
- scrollbar-color: black;
- }
-
- .scrollbar-black\\/50 {
- scrollbar-color: #00000080;
- }"
- `)
-
- expect(
- optimizeCss(
- await run([
- 'scrollbar-2/50',
- 'scrollbar-[2px]/50',
- 'scrollbar-[length:var(--my-width)]/50',
- ]),
- ).trim(),
- ).toEqual('')
- })
-
- test('functional utilities with `type: color` automatically support opacity', async () => {
- async function run(candidates: string[]) {
- let compiled = await compile(
- css`
- @plugin "my-plugin";
-
- @tailwind utilities;
-
- @theme reference {
- --breakpoint-lg: 1024px;
- }
- `,
- {
- async loadPlugin() {
- return ({ matchUtilities }: PluginAPI) => {
- matchUtilities(
- {
- scrollbar: (value) => ({ 'scrollbar-color': value }),
- },
- {
- type: ['color', 'any'],
- values: {
- black: 'black',
- },
- },
- )
- }
- },
- },
- )
-
- return compiled.build(candidates)
- }
-
- expect(
- optimizeCss(
- await run([
- 'scrollbar-current',
- 'scrollbar-current/45',
- 'scrollbar-black',
- 'scrollbar-black/33',
- 'scrollbar-black/[50%]',
- 'scrollbar-[var(--my-color)]/[25%]',
- ]),
- ).trim(),
- ).toMatchInlineSnapshot(`
- ".scrollbar-\\[var\\(--my-color\\)\\]\\/\\[25\\%\\] {
- scrollbar-color: color-mix(in srgb, var(--my-color) 25%, transparent);
- }
-
- .scrollbar-black {
- scrollbar-color: black;
- }
-
- .scrollbar-black\\/33 {
- scrollbar-color: #00000054;
- }
-
- .scrollbar-black\\/\\[50\\%\\] {
- scrollbar-color: #00000080;
- }
-
- .scrollbar-current {
- scrollbar-color: currentColor;
- }
-
- .scrollbar-current\\/45 {
- scrollbar-color: color-mix(in srgb, currentColor 45%, transparent);
- }"
- `)
- })
-
- test('functional utilities with explicit modifiers', async () => {
- async function run(candidates: string[]) {
- let compiled = await compile(
- css`
- @plugin "my-plugin";
-
- @tailwind utilities;
-
- @theme reference {
- --breakpoint-lg: 1024px;
- --opacity-my-opacity: 0.5;
- }
- `,
- {
- async loadPlugin() {
- return ({ matchUtilities }: PluginAPI) => {
- matchUtilities(
- {
- scrollbar: (value, { modifier }) => ({
- '--modifier': modifier ?? 'none',
- 'scrollbar-width': value,
- }),
- },
- {
- type: ['any'],
- values: {},
- modifiers: {
- foo: 'foo',
- },
- },
- )
- }
- },
- },
- )
-
- return compiled.build(candidates)
- }
-
- expect(
- optimizeCss(
- await run(['scrollbar-[12px]', 'scrollbar-[12px]/foo', 'scrollbar-[12px]/bar']),
- ).trim(),
- ).toMatchInlineSnapshot(`
- ".scrollbar-\\[12px\\] {
- --modifier: none;
- scrollbar-width: 12px;
- }
-
- .scrollbar-\\[12px\\]\\/foo {
- --modifier: foo;
- scrollbar-width: 12px;
- }"
- `)
- })
-
- test('functional utilities support `@apply`', async () => {
- let compiled = await compile(
- css`
- @plugin "my-plugin";
- @layer utilities {
- @tailwind utilities;
- }
-
- @theme reference {
- --breakpoint-lg: 1024px;
- }
- `,
- {
- async loadPlugin() {
- return ({ matchUtilities }: PluginAPI) => {
- matchUtilities(
- {
- foo: (value) => ({
- '--foo': value,
- [`@apply flex`]: {},
- }),
- },
- {
- values: {
- bar: 'bar',
- },
- },
- )
- }
- },
- },
- )
-
- expect(
- optimizeCss(compiled.build(['foo-bar', 'lg:foo-bar', 'foo-[12px]', 'lg:foo-[12px]'])).trim(),
- ).toMatchInlineSnapshot(`
- "@layer utilities {
- .foo-\\[12px\\] {
- --foo: 12px;
- display: flex;
- }
-
- .foo-bar {
- --foo: bar;
- display: flex;
- }
-
- @media (width >= 1024px) {
- .lg\\:foo-\\[12px\\] {
- --foo: 12px;
- display: flex;
- }
- }
-
- @media (width >= 1024px) {
- .lg\\:foo-bar {
- --foo: bar;
- display: flex;
- }
- }
- }"
- `)
- })
-
- test('throws on custom utilities with an invalid name', async () => {
- await expect(() => {
- return compile(
- css`
- @plugin "my-plugin";
- @layer utilities {
- @tailwind utilities;
- }
-
- @theme reference {
- --breakpoint-lg: 1024px;
- }
- `,
- {
- async loadPlugin() {
- return ({ matchUtilities }: PluginAPI) => {
- matchUtilities({
- '.text-trim > *': () => ({
- 'text-box-trim': 'both',
- 'text-box-edge': 'cap alphabetic',
- }),
- })
- }
- },
- },
- )
- }).rejects.toThrowError(/invalid utility name/)
- })
-})
diff --git a/packages/tailwindcss/tsup.config.ts b/packages/tailwindcss/tsup.config.ts
index 77ccda19b28e..25803da45289 100644
--- a/packages/tailwindcss/tsup.config.ts
+++ b/packages/tailwindcss/tsup.config.ts
@@ -3,7 +3,6 @@ import { defineConfig } from 'tsup'
export default defineConfig([
{
format: ['esm', 'cjs'],
- clean: true,
minify: true,
dts: true,
entry: {
@@ -16,6 +15,8 @@ export default defineConfig([
dts: true,
entry: {
plugin: 'src/plugin.ts',
+ colors: 'src/compat/colors.ts',
+ 'default-theme': 'src/compat/default-theme.ts',
},
},
{
@@ -24,6 +25,8 @@ export default defineConfig([
dts: true,
entry: {
plugin: 'src/plugin.cts',
+ colors: 'src/compat/colors.cts',
+ 'default-theme': 'src/compat/default-theme.cts',
},
},
])
diff --git a/playgrounds/vite/src/forms.js b/playgrounds/vite/src/forms.js
new file mode 100644
index 000000000000..f288ddfa0763
--- /dev/null
+++ b/playgrounds/vite/src/forms.js
@@ -0,0 +1 @@
+module.exports = require('@tailwindcss/forms')
diff --git a/playgrounds/vite/src/typography.js b/playgrounds/vite/src/typography.js
new file mode 100644
index 000000000000..711af27f791f
--- /dev/null
+++ b/playgrounds/vite/src/typography.js
@@ -0,0 +1 @@
+module.exports = require('@tailwindcss/typography')
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7f7cee20f35c..a916100c7276 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1249,7 +1249,6 @@ packages:
bun@1.1.22:
resolution: {integrity: sha512-G2HCPhzhjDc2jEDkZsO9vwPlpHrTm7a8UVwx9oNS5bZqo5OcSK5GPuWYDWjj7+37bRk5OVLfeIvUMtSrbKeIjQ==}
- cpu: [arm64, x64]
os: [darwin, linux, win32]
hasBin: true
@@ -4078,7 +4077,7 @@ snapshots:
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0)
- eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
+ eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0)
eslint-plugin-react: 7.35.0(eslint@8.57.0)
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0)
@@ -4102,7 +4101,7 @@ snapshots:
enhanced-resolve: 5.17.1
eslint: 8.57.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
- eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
+ eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.6
is-core-module: 2.15.0
@@ -4124,7 +4123,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
+ eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
dependencies:
array-includes: 3.1.8
array.prototype.findlastindex: 1.2.5