├── .gitignore
├── .github
├── dependabot.yml
├── tailwindcss-mark.svg
└── workflows
│ └── nodejs.yml
├── .editorconfig
├── package.json
├── LICENSE
├── src
├── filters.js
├── index.js
└── index.test.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | coverage
3 | node_modules
4 | npm-debug.log
5 | yarn-error.log
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | schedule:
6 | interval: weekly
7 | open-pull-requests-limit: 10
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/.github/tailwindcss-mark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: build
5 |
6 | on:
7 | push:
8 | branches: [main]
9 | pull_request:
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | matrix:
17 | node-version: [18, 20, 22]
18 |
19 | steps:
20 | - uses: actions/checkout@v4
21 | - name: Use Node.js ${{ matrix.node-version }}
22 | uses: actions/setup-node@v4
23 | with:
24 | node-version: ${{ matrix.node-version }}
25 | - run: npm ci
26 | - run: npm test
27 | env:
28 | CI: true
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tailwindcss-preset-email",
3 | "version": "1.4.1",
4 | "description": "Tailwind CSS config preset for HTML emails",
5 | "license": "MIT",
6 | "repository": "https://github.com/maizzle/tailwindcss-preset-email",
7 | "main": "src/index.js",
8 | "files": [
9 | "src"
10 | ],
11 | "scripts": {
12 | "dev": "vitest",
13 | "release": "npx np",
14 | "test": "vitest run --coverage"
15 | },
16 | "devDependencies": {
17 | "@vitest/coverage-v8": "^3.0.5",
18 | "postcss": "^8.4.49",
19 | "vitest": "^3.0.4"
20 | },
21 | "dependencies": {
22 | "tailwindcss-email-variants": "^3.0.3",
23 | "tailwindcss-mso": "^2.0.1"
24 | },
25 | "peerDependencies": {
26 | "tailwindcss": ">=3.4.17"
27 | },
28 | "keywords": [
29 | "email-css",
30 | "email-styles",
31 | "html-email",
32 | "responsive-email",
33 | "maizzle",
34 | "tailwindcss"
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Cosmin Popovici
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/filters.js:
--------------------------------------------------------------------------------
1 | const plugin = require('tailwindcss/plugin')
2 |
3 | module.exports = {
4 | disabledFilterPlugins: {
5 | blur: false,
6 | brightness: false,
7 | contrast: false,
8 | dropShadow: false,
9 | grayscale: false,
10 | hueRotate: false,
11 | invert: false,
12 | saturate: false,
13 | sepia: false,
14 | backdropBlur: false,
15 | backdropBrightness: false,
16 | backdropContrast: false,
17 | backdropGrayscale: false,
18 | backdropHueRotate: false,
19 | backdropInvert: false,
20 | backdropOpacity: false,
21 | backdropSaturate: false,
22 | backdropSepia: false,
23 | },
24 | blur: plugin(function({ matchUtilities, theme }) {
25 | matchUtilities(
26 | {
27 | blur: (value) => ({
28 | filter: `blur(${value})`
29 | }),
30 | },
31 | { values: theme('blur', ) }
32 | )
33 | }),
34 | brightness: plugin(function({ matchUtilities, theme }) {
35 | matchUtilities(
36 | {
37 | brightness: (value) => ({
38 | filter: `brightness(${value})`
39 | }),
40 | },
41 | { values: theme('brightness') }
42 | )
43 | }),
44 | contrast: plugin(function({ matchUtilities, theme }) {
45 | matchUtilities(
46 | {
47 | contrast: (value) => ({
48 | filter: `contrast(${value})`
49 | }),
50 | },
51 | { values: theme('contrast') }
52 | )
53 | }),
54 | dropShadow: plugin(function({ matchUtilities, theme }) {
55 | matchUtilities(
56 | {
57 | 'drop-shadow': (value) => ({
58 | filter: Array.isArray(value)
59 | ? value.map((v) => `drop-shadow(${v})`).join(' ')
60 | : `drop-shadow(${value})`
61 | }),
62 | },
63 | { values: theme('dropShadow') }
64 | )
65 | }),
66 | grayscale: plugin(function({ matchUtilities, theme }) {
67 | matchUtilities(
68 | {
69 | grayscale: (value) => ({
70 | filter: `grayscale(${value})`
71 | }),
72 | },
73 | { values: theme('grayscale') }
74 | )
75 | }),
76 | hueRotate: plugin(function({ matchUtilities, theme }) {
77 | matchUtilities(
78 | {
79 | 'hue-rotate': (value) => ({
80 | filter: `hue-rotate(${value})`
81 | }),
82 | },
83 | { values: theme('hueRotate'), supportsNegativeValues: true }
84 | )
85 | }),
86 | invert: plugin(function({ matchUtilities, theme }) {
87 | matchUtilities(
88 | {
89 | invert: (value) => ({
90 | filter: `invert(${value})`
91 | }),
92 | },
93 | { values: theme('invert') }
94 | )
95 | }),
96 | saturate: plugin(function({ matchUtilities, theme }) {
97 | matchUtilities(
98 | {
99 | saturate: (value) => ({
100 | filter: `saturate(${value})`
101 | }),
102 | },
103 | { values: theme('saturate') }
104 | )
105 | }),
106 | sepia: plugin(function({ matchUtilities, theme }) {
107 | matchUtilities(
108 | {
109 | sepia: (value) => ({
110 | filter: `sepia(${value})`
111 | }),
112 | },
113 | { values: theme('sepia') }
114 | )
115 | }),
116 | backdropBlur: plugin(function({ matchUtilities, theme }) {
117 | matchUtilities(
118 | {
119 | 'backdrop-blur': (value) => ({
120 | backdropFilter: `blur(${value})`
121 | }),
122 | },
123 | { values: theme('backdropBlur') }
124 | )
125 | }),
126 | backdropBrightness: plugin(function({ matchUtilities, theme }) {
127 | matchUtilities(
128 | {
129 | 'backdrop-brightness': (value) => ({
130 | backdropFilter: `brightness(${value})`
131 | }),
132 | },
133 | { values: theme('backdropBrightness') }
134 | )
135 | }),
136 | backdropContrast: plugin(function({ matchUtilities, theme }) {
137 | matchUtilities(
138 | {
139 | 'backdrop-contrast': (value) => ({
140 | backdropFilter: `contrast(${value})`
141 | }),
142 | },
143 | { values: theme('backdropContrast') }
144 | )
145 | }),
146 | backdropGrayscale: plugin(function({ matchUtilities, theme }) {
147 | matchUtilities(
148 | {
149 | 'backdrop-grayscale': (value) => ({
150 | backdropFilter: `grayscale(${value})`
151 | }),
152 | },
153 | { values: theme('backdropGrayscale') }
154 | )
155 | }),
156 | backdropHueRotate: plugin(function({ matchUtilities, theme }) {
157 | matchUtilities(
158 | {
159 | 'backdrop-hue-rotate': (value) => ({
160 | backdropFilter: `hue-rotate(${value})`
161 | }),
162 | },
163 | { values: theme('backdropHueRotate'), supportsNegativeValues: true }
164 | )
165 | }),
166 | backdropInvert: plugin(function({ matchUtilities, theme }) {
167 | matchUtilities(
168 | {
169 | 'backdrop-invert': (value) => ({
170 | backdropFilter: `invert(${value})`
171 | }),
172 | },
173 | { values: theme('backdropInvert') }
174 | )
175 | }),
176 | backdropOpacity: plugin(function({ matchUtilities, theme }) {
177 | matchUtilities(
178 | {
179 | 'backdrop-opacity': (value) => ({
180 | backdropFilter: `opacity(${value})`
181 | }),
182 | },
183 | { values: theme('backdropOpacity') }
184 | )
185 | }),
186 | backdropSaturate: plugin(function({ matchUtilities, theme }) {
187 | matchUtilities(
188 | {
189 | 'backdrop-saturate': (value) => ({
190 | backdropFilter: `saturate(${value})`
191 | }),
192 | },
193 | { values: theme('backdropSaturate') }
194 | )
195 | }),
196 | backdropSepia: plugin(function({ matchUtilities, theme }) {
197 | matchUtilities(
198 | {
199 | 'backdrop-sepia': (value) => ({
200 | backdropFilter: `sepia(${value})`
201 | }),
202 | },
203 | { values: theme('backdropSepia') }
204 | )
205 | }),
206 | }
207 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const plugin = require('tailwindcss/plugin')
2 | const { disabledFilterPlugins, ...filterPlugins } = require('./filters')
3 |
4 | /** @type {import('tailwindcss').Config} */
5 | module.exports = {
6 | important: true,
7 | theme: {
8 | screens: {
9 | sm: {max: '600px'},
10 | xs: {max: '430px'},
11 | },
12 | extend: {
13 | colors: {
14 | black: '#000001',
15 | white: '#fffffe',
16 | },
17 | spacing: {
18 | screen: '100vw',
19 | full: '100%',
20 | 0: '0',
21 | 0.5: '2px',
22 | 1: '4px',
23 | 1.5: '6px',
24 | 2: '8px',
25 | 2.5: '10px',
26 | 3: '12px',
27 | 3.5: '14px',
28 | 4: '16px',
29 | 4.5: '18px',
30 | 5: '20px',
31 | 5.5: '22px',
32 | 6: '24px',
33 | 6.5: '26px',
34 | 7: '28px',
35 | 7.5: '30px',
36 | 8: '32px',
37 | 8.5: '34px',
38 | 9: '36px',
39 | 9.5: '38px',
40 | 10: '40px',
41 | 11: '44px',
42 | 12: '48px',
43 | 14: '56px',
44 | 16: '64px',
45 | 18: '72px',
46 | 20: '80px',
47 | 22: '88px',
48 | 24: '96px',
49 | 26: '104px',
50 | 28: '112px',
51 | 30: '120px',
52 | 32: '128px',
53 | 34: '136px',
54 | 36: '144px',
55 | 38: '152px',
56 | 40: '160px',
57 | 42: '168px',
58 | 44: '176px',
59 | 46: '184px',
60 | 48: '192px',
61 | 50: '200px',
62 | 52: '208px',
63 | 54: '216px',
64 | 56: '224px',
65 | 58: '232px',
66 | 60: '240px',
67 | 62: '248px',
68 | 64: '256px',
69 | 66: '264px',
70 | 68: '272px',
71 | 70: '280px',
72 | 72: '288px',
73 | 74: '296px',
74 | 76: '304px',
75 | 78: '312px',
76 | 80: '320px',
77 | 82: '328px',
78 | 84: '336px',
79 | 86: '344px',
80 | 88: '352px',
81 | 90: '360px',
82 | 92: '368px',
83 | 94: '376px',
84 | 96: '384px',
85 | },
86 | borderRadius: {
87 | none: '0px',
88 | sm: '2px',
89 | DEFAULT: '4px',
90 | md: '6px',
91 | lg: '8px',
92 | xl: '12px',
93 | '2xl': '16px',
94 | '3xl': '24px',
95 | },
96 | boxShadow: {
97 | sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
98 | DEFAULT: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
99 | md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
100 | lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
101 | xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
102 | '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
103 | inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.05)',
104 | },
105 | dropShadow: {
106 | sm: '0 1px 1px rgba(0, 0, 0, 0.05)',
107 | DEFAULT: [
108 | '0 1px 2px rgba(0, 0, 0, 0.1)',
109 | '0 1px 1px rgba(0, 0, 0, 0.06)',
110 | ],
111 | md: [
112 | '0 4px 3px rgba(0, 0, 0, 0.07)',
113 | '0 2px 2px rgba(0, 0, 0, 0.06)',
114 | ],
115 | lg: [
116 | '0 10px 8px rgba(0, 0, 0, 0.04)',
117 | '0 4px 3px rgba(0, 0, 0, 0.1)',
118 | ],
119 | xl: [
120 | '0 20px 13px rgba(0, 0, 0, 0.03)',
121 | '0 8px 5px rgba(0, 0, 0, 0.08)',
122 | ],
123 | '2xl': '0 25px 25px rgba(0, 0, 0, 0.15)',
124 | none: '0 0 #000',
125 | },
126 | fontFamily: {
127 | inter: ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', '"Segoe UI"', 'sans-serif'],
128 | sans: ['ui-sans-serif', 'system-ui', '-apple-system', '"Segoe UI"', 'sans-serif'],
129 | serif: ['ui-serif', 'Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'],
130 | mono: ['ui-monospace', 'Menlo', 'Consolas', 'monospace'],
131 | },
132 | fontSize: {
133 | 0: '0',
134 | xxs: '11px',
135 | xs: '12px',
136 | '2xs': '13px',
137 | sm: '14px',
138 | '2sm': '15px',
139 | base: '16px',
140 | lg: '18px',
141 | xl: '20px',
142 | '2xl': '24px',
143 | '3xl': '30px',
144 | '4xl': '36px',
145 | '5xl': '48px',
146 | '6xl': '60px',
147 | '7xl': '72px',
148 | '8xl': '96px',
149 | '9xl': '128px',
150 | },
151 | letterSpacing: theme => ({
152 | ...theme('width'),
153 | }),
154 | lineHeight: theme => ({
155 | ...theme('width'),
156 | }),
157 | maxWidth: theme => ({
158 | ...theme('width'),
159 | xs: '160px',
160 | sm: '192px',
161 | md: '224px',
162 | lg: '256px',
163 | xl: '288px',
164 | '2xl': '336px',
165 | '3xl': '384px',
166 | '4xl': '448px',
167 | '5xl': '512px',
168 | '6xl': '576px',
169 | '7xl': '640px',
170 | }),
171 | minHeight: theme => ({
172 | ...theme('width'),
173 | }),
174 | minWidth: theme => ({
175 | ...theme('width'),
176 | }),
177 | },
178 | },
179 | // We disable all core plugins that we redefine
180 | corePlugins: {
181 | preflight: false,
182 | backgroundOpacity: false,
183 | borderOpacity: false,
184 | borderSpacing: false,
185 | boxShadow: false,
186 | boxShadowColor: false,
187 | divideOpacity: false,
188 | placeholderOpacity: false,
189 | textOpacity: false,
190 | textDecoration: false,
191 | ...disabledFilterPlugins,
192 | },
193 | plugins: [
194 | plugin(function({ matchUtilities, addUtilities, theme }) {
195 | // Border-spacing utilities
196 | matchUtilities(
197 | {
198 | 'border-spacing': (value) => ({
199 | 'border-spacing': value,
200 | }),
201 | 'border-spacing-y': (value) => ({
202 | 'border-spacing': `0 ${value}`,
203 | }),
204 | 'border-spacing-x': (value) => ({
205 | 'border-spacing': `${value} 0`,
206 | }),
207 | },
208 | { values: theme('borderSpacing', ) }
209 | )
210 |
211 | // Box-shadow utilities
212 | matchUtilities(
213 | {
214 | shadow: value => ({
215 | boxShadow: value
216 | }),
217 | },
218 | {
219 | values: theme('boxShadow')
220 | }
221 | )
222 |
223 | // Text decoration utilities
224 | addUtilities({
225 | '.underline': { 'text-decoration': 'underline' },
226 | '.overline': { 'text-decoration': 'overline' },
227 | '.line-through': { 'text-decoration': 'line-through' },
228 | '.no-underline': { 'text-decoration': 'none' },
229 | })
230 | }),
231 | // Filters
232 | ...Object.values(filterPlugins),
233 | // MSO utilities
234 | require('tailwindcss-mso'),
235 | // Email client targeting variants
236 | require('tailwindcss-email-variants'),
237 | ],
238 | }
239 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
Tailwind CSS Email Preset
4 |
Tailwind CSS config preset for HTML emails
5 |
6 |
7 | ## About
8 |
9 | This is a Tailwind CSS v3.x config preset that changes some utility classes to use values that are better supported in email clients. It also includes plugins that generate utility classes that are useful for building HTML emails.
10 |
11 | For Tailwind CSS v4.x, see [maizzle/tailwindcss](https://github.com/maizzle/tailwindcss).
12 |
13 | ## Installation
14 |
15 | ```bash
16 | npm install tailwindcss-preset-email
17 | ```
18 |
19 | ## Usage
20 |
21 | ```js
22 | // tailwind.config.js
23 | module.exports = {
24 | presets: [
25 | require('tailwindcss-preset-email'),
26 | ],
27 | }
28 | ```
29 |
30 | ### Customization
31 |
32 | You may override the preset by configuring Tailwind as you'd normally do.
33 |
34 | ```js
35 | // tailwind.config.js
36 | module.exports = {
37 | presets: [
38 | require('tailwindcss-preset-email'),
39 | ],
40 | theme: {
41 | screens: {
42 | sm: '640px',
43 | md: '768px',
44 | },
45 | },
46 | }
47 | ```
48 |
49 | ### Configuration
50 |
51 | The preset is a custom Tailwind CSS config that changes utility classes to use values that are better supported in email clients, either right in the config or through plugins.
52 |
53 | ### important
54 |
55 | The `important` key is set to `true`.
56 |
57 | HTML emails often use inline CSS, so this ensures that any CSS generated by Tailwind uses `!important` to override inline styles (unless the inline styles themselves use `!important`).
58 |
59 | ### screens
60 |
61 | The `screens` config uses a desktop-first approach now:
62 |
63 | ```js
64 | {
65 | sm: {max: '600px'},
66 | xs: {max: '430px'},
67 | }
68 | ```
69 |
70 | When overriding `screens`, make sure the larger values are at the top of the object:
71 |
72 | ```js
73 | {
74 | md: {max: '768px'},
75 | sm: {max: '600px'},
76 | xs: {max: '430px'},
77 | }
78 | ```
79 |
80 | ### colors
81 |
82 | The `black` and `white` color values are updated to prevent some email clients from automatically inverting them in dark mode.
83 |
84 | ```js
85 | colors: {
86 | black: '#000001',
87 | white: '#fffffe',
88 | }
89 | ```
90 |
91 | ### spacing
92 |
93 | The `spacing` scale has been updated to use `px` values instead of `rem`.
94 |
95 | ```js
96 | spacing: {
97 | screen: '100vw',
98 | full: '100%',
99 | 0: '0',
100 | 0.5: '2px',
101 | 1: '4px',
102 | 1.5: '6px',
103 | 2: '8px',
104 | // ...
105 | }
106 | ```
107 |
108 | ### borderRadius
109 |
110 | The `borderRadius` scale has been updated to use `px` values instead of `rem`.
111 |
112 | ```js
113 | borderRadius: {
114 | none: '0px',
115 | sm: '2px',
116 | DEFAULT: '4px',
117 | md: '6px',
118 | lg: '8px',
119 | xl: '12px',
120 | '2xl': '16px',
121 | '3xl': '24px',
122 | }
123 | ```
124 |
125 | ### boxShadow
126 |
127 | `boxShadow` utilities use the exact values from your config.
128 |
129 | ```js
130 | boxShadow: {
131 | sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
132 | DEFAULT: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
133 | md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
134 | lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
135 | xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
136 | '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
137 | inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.05)',
138 | }
139 | ```
140 |
141 | ### dropShadow
142 |
143 | Although not that much used in HTML email, the `dropShadow` utilities are now generated using the exact values from your config.
144 |
145 | It defaults to this:
146 |
147 | ```js
148 | dropShadow: {
149 | sm: '0 1px 1px rgba(0, 0, 0, 0.05)',
150 | DEFAULT: [
151 | '0 1px 2px rgba(0, 0, 0, 0.1)',
152 | '0 1px 1px rgba(0, 0, 0, 0.06)',
153 | ],
154 | md: [
155 | '0 4px 3px rgba(0, 0, 0, 0.07)',
156 | '0 2px 2px rgba(0, 0, 0, 0.06)',
157 | ],
158 | lg: [
159 | '0 10px 8px rgba(0, 0, 0, 0.04)',
160 | '0 4px 3px rgba(0, 0, 0, 0.1)',
161 | ],
162 | xl: [
163 | '0 20px 13px rgba(0, 0, 0, 0.03)',
164 | '0 8px 5px rgba(0, 0, 0, 0.08)',
165 | ],
166 | '2xl': '0 25px 25px rgba(0, 0, 0, 0.15)',
167 | none: '0 0 #000',
168 | }
169 | ```
170 |
171 | ### fontFamily
172 |
173 | `fontFamily` font stacks have been simplified to use web-safe fonts only.
174 |
175 | ```js
176 | fontFamily: {
177 | sans: ['ui-sans-serif', 'system-ui', '-apple-system', '"Segoe UI"', 'sans-serif'],
178 | serif: ['ui-serif', 'Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'],
179 | mono: ['ui-monospace', 'Menlo', 'Consolas', 'monospace'],
180 | }
181 | ```
182 |
183 | ### fontSize
184 |
185 | `fontSize` values have been updated to use `px` values instead of `rem`.
186 |
187 | ```js
188 | fontSize: {
189 | 0: '0',
190 | xxs: '11px',
191 | xs: '12px',
192 | '2xs': '13px',
193 | sm: '14px',
194 | '2sm': '15px',
195 | base: '16px',
196 | lg: '18px',
197 | xl: '20px',
198 | '2xl': '24px',
199 | '3xl': '30px',
200 | '4xl': '36px',
201 | '5xl': '48px',
202 | '6xl': '60px',
203 | '7xl': '72px',
204 | '8xl': '96px',
205 | '9xl': '128px',
206 | }
207 | ```
208 |
209 | ### letterSpacing
210 |
211 | `letterSpacing` values have been updated to use values from your `width` scale.
212 |
213 | ```js
214 | letterSpacing: theme => ({
215 | ...theme('width'),
216 | })
217 | ```
218 |
219 | ### lineHeight
220 |
221 | Likewise, `lineHeight` values have been updated to use values from your `width` scale.
222 |
223 | ```js
224 | lineHeight: theme => ({
225 | ...theme('width'),
226 | })
227 | ```
228 |
229 | ### maxWidth
230 |
231 | `maxWidth` values have been updated to use `px` values instead of `rem`, and now also use values from your `width` scale.
232 |
233 | ```js
234 | maxWidth: theme => ({
235 | ...theme('width'),
236 | xs: '160px',
237 | sm: '192px',
238 | md: '224px',
239 | lg: '256px',
240 | xl: '288px',
241 | '2xl': '336px',
242 | '3xl': '384px',
243 | '4xl': '448px',
244 | '5xl': '512px',
245 | '6xl': '576px',
246 | '7xl': '640px',
247 | })
248 | ```
249 |
250 | ### minHeight
251 |
252 | `minHeight` values have been updated to use values from your `width` scale.
253 |
254 | ```js
255 | minHeight: theme => ({
256 | ...theme('width'),
257 | })
258 | ```
259 |
260 | ### minWidth
261 |
262 | `minWidth` values have been updated to use values from your `width` scale.
263 |
264 | ```js
265 | minWidth: theme => ({
266 | ...theme('width'),
267 | })
268 | ```
269 |
270 | ## Plugins
271 |
272 | The preset includes the following plugins:
273 |
274 | ### tailwindcss-mso
275 |
276 | Used for generating classes that are only supported by Microsoft Outlook's Word rendering engine, for Outlook on Windows (versions 2007 and up).
277 |
278 | Documentation: https://github.com/maizzle/tailwindcss-mso
279 |
280 | ### tailwindcss-email-variants
281 |
282 | A Tailwind CSS plugin that provides variants for email client targeting hacks used in HTML emails.
283 |
284 | Documentation: https://github.com/maizzle/tailwindcss-email-variants
285 |
286 | ### borderSpacing
287 |
288 | A custom plugin that generates `border-spacing` utilities that use static values instead of CSS variables.
289 |
290 | Here's a diff of the output between Tailwind's original and the custom plugin:
291 |
292 | ```diff
293 | - .border-spacing-x-1 {
294 | - --tw-border-spacing-x: 4px;
295 | - border-spacing: var(--tw-border-spacing-x) var(--tw-border-spacing-y);
296 | - }
297 | + .border-spacing-x-1 {
298 | + border-spacing: 4px 0
299 | + }
300 | ```
301 |
302 | ### boxShadow
303 |
304 | A custom plugin that generates `box-shadow` utilities that use static values instead of CSS variables.
305 |
306 | The downside to this is that shadow color utilities are disabled too.
307 |
308 | ```diff
309 | - .shadow-sm {
310 | - --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
311 | - --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
312 | - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
313 | - }
314 | + .shadow-sm {
315 | + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05)
316 | + }
317 | ```
318 |
319 | ### Filters
320 |
321 | The following plugins that generate utilities for CSS filters have been updated to use static values instead of CSS variables:
322 |
323 | - blur
324 | - brightness
325 | - contrast
326 | - dropShadow
327 | - grayscale
328 | - hueRotate
329 | - invert
330 | - saturate
331 | - sepia
332 | - backdropBlur
333 | - backdropBrightness
334 | - backdropContrast
335 | - backdropGrayscale
336 | - backdropHueRotate
337 | - backdropInvert
338 | - backdropOpacity
339 | - backdropSaturate
340 | - backdropSepia
341 |
342 | ### textDecoration
343 |
344 | A custom plugin that generates `text-decoration` utilities that use the `text-decoration` property instead of `text-decoration-line`, which has poor support in email clients.
345 |
346 | Here's a diff of the output between Tailwind's original and the custom plugin:
347 |
348 | ```diff
349 | .underline {
350 | - text-decoration-line: underline;
351 | + text-decoration: underline
352 | }
353 | ```
354 |
--------------------------------------------------------------------------------
/src/index.test.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import postcss from 'postcss'
3 | import emailPreset from '.'
4 | import { describe, expect, test } from 'vitest'
5 | import tailwindcss from 'tailwindcss'
6 |
7 | // Custom CSS matcher
8 | expect.extend({
9 | // Compare two CSS strings with all whitespace removed
10 | // This is probably naive but it's fast and works well enough.
11 | toMatchCss(received, argument) {
12 | function stripped(string_) {
13 | return string_
14 | .replaceAll(/\s/g, '')
15 | .replaceAll(';', '')
16 | }
17 |
18 | const pass = stripped(received) === stripped(argument)
19 |
20 | return {
21 | pass,
22 | actual: received,
23 | expected: argument,
24 | message: () => pass ? 'All good!' : 'CSS does not match',
25 | }
26 | }
27 | })
28 |
29 | // Function to run the plugin
30 | function run(config, css = '@tailwind utilities', plugin = tailwindcss) {
31 | let { currentTestName } = expect.getState()
32 |
33 | config = {
34 | ...{
35 | presets: [emailPreset],
36 | important: false,
37 | },
38 | ...config,
39 | }
40 |
41 | return postcss(plugin(config)).process(css, {
42 | from: `${path.resolve(__filename)}?test=${currentTestName}`,
43 | })
44 | }
45 |
46 | test('borderSpacing', () => {
47 | const config = {
48 | content: [
49 | {
50 | raw: String.raw`
51 |
52 |
53 |
54 | `
55 | }
56 | ],
57 | }
58 |
59 | return run(config).then(result => {
60 | expect(result.css).toMatchCss(String.raw`
61 | .border-spacing-0 {
62 | border-spacing: 0
63 | }
64 | .border-spacing-x-1 {
65 | border-spacing: 4px 0
66 | }
67 | .border-spacing-y-1 {
68 | border-spacing: 0 4px
69 | }
70 | `)
71 | })
72 | })
73 |
74 | test('boxShadow', () => {
75 | const config = {
76 | content: [
77 | {
78 | raw: String.raw`
79 |
80 | `
81 | }
82 | ],
83 | }
84 |
85 | return run(config).then(result => {
86 | expect(result.css).toMatchCss(String.raw`
87 | .shadow {
88 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)
89 | }
90 | `)
91 | })
92 | })
93 |
94 | test('textDecoration', () => {
95 | const config = {
96 | content: [
97 | {
98 | raw: String.raw`
99 |
100 |
101 |
102 |
103 | `
104 | }
105 | ],
106 | }
107 |
108 | return run(config).then(result => {
109 | expect(result.css).toMatchCss(String.raw`
110 | .underline {
111 | text-decoration: underline
112 | }
113 | .overline {
114 | text-decoration: overline
115 | }
116 | .line-through {
117 | text-decoration: line-through
118 | }
119 | .no-underline {
120 | text-decoration: none
121 | }
122 | `)
123 | })
124 | })
125 |
126 | describe('Filters', () => {
127 | test('blur', () => {
128 | const config = {
129 | content: [
130 | {
131 | raw: String.raw`
132 |
133 |
134 | `
135 | }
136 | ],
137 | }
138 |
139 | return run(config).then(result => {
140 | expect(result.css).toMatchCss(String.raw`
141 | .blur-\[2px\] {
142 | filter: blur(2px)
143 | }
144 | .blur-xl {
145 | filter: blur(24px)
146 | }
147 | `)
148 | })
149 | })
150 |
151 | test('brightness', () => {
152 | const config = {
153 | content: [
154 | {
155 | raw: String.raw`
156 |
157 |
158 | `
159 | }
160 | ],
161 | }
162 |
163 | return run(config).then(result => {
164 | expect(result.css).toMatchCss(String.raw`
165 | .brightness-50 {
166 | filter: brightness(.5)
167 | }
168 | .brightness-\[\.33\] {
169 | filter: brightness(.33)
170 | }
171 | `)
172 | })
173 | })
174 |
175 | test('contrast', () => {
176 | const config = {
177 | content: [
178 | {
179 | raw: String.raw`
180 |
181 |
182 | `
183 | }
184 | ],
185 | }
186 |
187 | return run(config).then(result => {
188 | expect(result.css).toMatchCss(String.raw`
189 | .contrast-50 {
190 | filter: contrast(.5)
191 | }
192 | .contrast-\[\.33\] {
193 | filter: contrast(.33)
194 | }
195 | `)
196 | })
197 | })
198 |
199 | test('dropShadow', () => {
200 | const config = {
201 | content: [
202 | {
203 | raw: String.raw`
204 |
205 |
206 |
207 | `
208 | }
209 | ],
210 | }
211 |
212 | return run(config).then(result => {
213 | expect(result.css).toMatchCss(String.raw`
214 | .drop-shadow {
215 | filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)) drop-shadow(0 1px 1px rgba(0, 0, 0, 0.06))
216 | }
217 | .drop-shadow-\[0_35px_35px_rgba\(0\2c 0\2c 0\2c 0\.25\)\] {
218 | filter: drop-shadow(0 35px 35px rgba(0,0,0,0.25))
219 | }
220 | .drop-shadow-sm {
221 | filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.05))
222 | }
223 | `)
224 | })
225 | })
226 |
227 | test('grayscale', () => {
228 | const config = {
229 | content: [
230 | {
231 | raw: String.raw`
232 |
233 |
234 |
235 | `
236 | }
237 | ],
238 | }
239 |
240 | return run(config).then(result => {
241 | expect(result.css).toMatchCss(String.raw`
242 | .grayscale {
243 | filter: grayscale(100%)
244 | }
245 | .grayscale-0 {
246 | filter: grayscale(0)
247 | }
248 | .grayscale-\[50\%\] {
249 | filter: grayscale(50%)
250 | }
251 | `)
252 | })
253 | })
254 |
255 | test('hueRotate', () => {
256 | const config = {
257 | content: [
258 | {
259 | raw: String.raw`
260 |
261 |
262 |
263 | `
264 | }
265 | ],
266 | }
267 |
268 | return run(config).then(result => {
269 | expect(result.css).toMatchCss(String.raw`
270 | .-hue-rotate-60 {
271 | filter: hue-rotate(-60deg)
272 | }
273 | .hue-rotate-180 {
274 | filter: hue-rotate(180deg)
275 | }
276 | .hue-rotate-\[90deg\] {
277 | filter: hue-rotate(90deg)
278 | }
279 | `)
280 | })
281 | })
282 |
283 | test('invert', () => {
284 | const config = {
285 | content: [
286 | {
287 | raw: String.raw`
288 |
289 |
290 | `
291 | }
292 | ],
293 | }
294 |
295 | return run(config).then(result => {
296 | expect(result.css).toMatchCss(String.raw`
297 | .invert {
298 | filter: invert(100%)
299 | }
300 | .invert-\[\.25\] {
301 | filter: invert(.25)
302 | }
303 | `)
304 | })
305 | })
306 |
307 | test('saturate', () => {
308 | const config = {
309 | content: [
310 | {
311 | raw: String.raw`
312 |
313 |
314 | `
315 | }
316 | ],
317 | }
318 |
319 | return run(config).then(result => {
320 | expect(result.css).toMatchCss(String.raw`
321 | .saturate-0 {
322 | filter: saturate(0)
323 | }
324 | .saturate-\[\.25\] {
325 | filter: saturate(.25)
326 | }
327 | `)
328 | })
329 | })
330 |
331 | test('sepia', () => {
332 | const config = {
333 | content: [
334 | {
335 | raw: String.raw`
336 |
337 |
338 | `
339 | }
340 | ],
341 | }
342 |
343 | return run(config).then(result => {
344 | expect(result.css).toMatchCss(String.raw`
345 | .sepia-0 {
346 | filter: sepia(0)
347 | }
348 | .sepia-\[\.25\] {
349 | filter: sepia(.25)
350 | }
351 | `)
352 | })
353 | })
354 |
355 | test('backdropBlur', () => {
356 | const config = {
357 | content: [
358 | {
359 | raw: String.raw`
360 |
361 |
362 | `
363 | }
364 | ],
365 | }
366 |
367 | return run(config).then(result => {
368 | expect(result.css).toMatchCss(String.raw`
369 | .backdrop-blur-\[2px\] {
370 | backdrop-filter: blur(2px)
371 | }
372 | .backdrop-blur-sm {
373 | backdrop-filter: blur(4px)
374 | }
375 | `)
376 | })
377 | })
378 |
379 | test('backdropBrightness', () => {
380 | const config = {
381 | content: [
382 | {
383 | raw: String.raw`
384 |
385 |
386 | `
387 | }
388 | ],
389 | }
390 |
391 | return run(config).then(result => {
392 | expect(result.css).toMatchCss(String.raw`
393 | .backdrop-brightness-50 {
394 | backdrop-filter: brightness(.5)
395 | }
396 | .backdrop-brightness-\[1\.75\] {
397 | backdrop-filter: brightness(1.75)
398 | }
399 | `)
400 | })
401 | })
402 |
403 | test('backdropContrast', () => {
404 | const config = {
405 | content: [
406 | {
407 | raw: String.raw`
408 |
409 |
410 | `
411 | }
412 | ],
413 | }
414 |
415 | return run(config).then(result => {
416 | expect(result.css).toMatchCss(String.raw`
417 | .backdrop-contrast-50 {
418 | backdrop-filter: contrast(.5)
419 | }
420 | .backdrop-contrast-\[1\.75\] {
421 | backdrop-filter: contrast(1.75)
422 | }
423 | `)
424 | })
425 | })
426 |
427 | test('backdropGrayscale', () => {
428 | const config = {
429 | content: [
430 | {
431 | raw: String.raw`
432 |
433 |
434 | `
435 | }
436 | ],
437 | }
438 |
439 | return run(config).then(result => {
440 | expect(result.css).toMatchCss(String.raw`
441 | .backdrop-grayscale {
442 | backdrop-filter: grayscale(100%)
443 | }
444 | .backdrop-grayscale-\[\.5\] {
445 | backdrop-filter: grayscale(.5)
446 | }
447 | `)
448 | })
449 | })
450 |
451 | test('backdropHueRotate', () => {
452 | const config = {
453 | content: [
454 | {
455 | raw: String.raw`
456 |
457 |
458 |
459 | `
460 | }
461 | ],
462 | }
463 |
464 | return run(config).then(result => {
465 | expect(result.css).toMatchCss(String.raw`
466 | .-backdrop-hue-rotate-15 {
467 | backdrop-filter: hue-rotate(-15deg)
468 | }
469 | .backdrop-hue-rotate-15 {
470 | backdrop-filter: hue-rotate(15deg)
471 | }
472 | .backdrop-hue-rotate-\[35deg\] {
473 | backdrop-filter: hue-rotate(35deg)
474 | }
475 | `)
476 | })
477 | })
478 |
479 | test('backdropInvert', () => {
480 | const config = {
481 | content: [
482 | {
483 | raw: String.raw`
484 |
485 |
486 | `
487 | }
488 | ],
489 | }
490 |
491 | return run(config).then(result => {
492 | expect(result.css).toMatchCss(String.raw`
493 | .backdrop-invert {
494 | backdrop-filter: invert(100%)
495 | }
496 | .backdrop-invert-\[\.25\] {
497 | backdrop-filter: invert(.25)
498 | }
499 | `)
500 | })
501 | })
502 |
503 | test('backdropOpacity', () => {
504 | const config = {
505 | content: [
506 | {
507 | raw: String.raw`
508 |
509 |
510 | `
511 | }
512 | ],
513 | }
514 |
515 | return run(config).then(result => {
516 | expect(result.css).toMatchCss(String.raw`
517 | .backdrop-opacity-5 {
518 | backdrop-filter: opacity(0.05)
519 | }
520 | .backdrop-opacity-\[\.25\] {
521 | backdrop-filter: opacity(.25)
522 | }
523 | `)
524 | })
525 | })
526 |
527 | test('backdropSaturate', () => {
528 | const config = {
529 | content: [
530 | {
531 | raw: String.raw`
532 |
533 |
534 | `
535 | }
536 | ],
537 | }
538 |
539 | return run(config).then(result => {
540 | expect(result.css).toMatchCss(String.raw`
541 | .backdrop-saturate-50 {
542 | backdrop-filter: saturate(.5)
543 | }
544 | .backdrop-saturate-\[\.25\] {
545 | backdrop-filter: saturate(.25)
546 | }
547 | `)
548 | })
549 | })
550 |
551 | test('backdropSepia', () => {
552 | const config = {
553 | content: [
554 | {
555 | raw: String.raw`
556 |
557 |
558 | `
559 | }
560 | ],
561 | }
562 |
563 | return run(config).then(result => {
564 | expect(result.css).toMatchCss(String.raw`
565 | .backdrop-sepia {
566 | backdrop-filter: sepia(100%)
567 | }
568 | .backdrop-sepia-\[\.25\] {
569 | backdrop-filter: sepia(.25)
570 | }
571 | `)
572 | })
573 | })
574 | })
575 |
--------------------------------------------------------------------------------