From fcf948f8c8d7863efe66817a65f9e86a66c07019 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 21 Nov 2024 11:21:31 +0100 Subject: [PATCH] Add consistent base styles for buttons and form controls (#15036) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR introduces consistent base styles for buttons and form controls in Tailwind CSS v4. ## Motivation In v3, form elements lack default styles, which can be confusing—especially when certain elements, like a text input without a placeholder or value, are rendered completely invisible on the page. The goal of this change is to provide reasonable default styles for buttons, inputs, selects, and textareas that are (mostly) consistent across all browsers while remaining easy to customize with your own styles. This improvement should make Tailwind more accessible for developers new to the framework and more convenient in scenarios where you need to quickly create demos (e.g., using Tailwind Play). ## Light and dark mode support These styles support both light and dark mode, achieved using the `light-dark()` CSS function. While browser support for this function is still somewhat limited, Lightning CSS transpiles it to a CSS variable-based approach that works in older browsers. For this approach to function correctly, a default `color-scheme` must be set in your CSS (as explained in [the Lightning CSS documentation](https://lightningcss.dev/transpilation.html#light-dark()-color-function)). This PR addresses this requirement by setting the `color-scheme` to `light` on the `html` element in Preflight. image image ## Breaking changes While we don’t expect these changes to significantly impact v3 users upgrading to v4, there may be minor differences for those relying on the simpler v3 styles. For example, Preflight now applies a `border-radius` to buttons and form controls. If you weren’t explicitly setting the border radius to `0` in your project, you’ll need to do so to restore the previous look. Thankfully, reverting to the v3 styles is straightforward—just add the following reset to your CSS: ```css @layer base { input, textarea, select, button { border: 0px solid; border-radius: 0; padding: 0; background-color: transparent; } } ``` It’s worth noting that this reset doesn't touch the `::file-selector-button` styles that were added in this PR. This is because it's not possible to reliably "undo" these styles and restore the original user-agent styles (which is what was used in v3), as these are different in each browser. However, these new styles actually match the defaults in most browsers pretty closely, so hopefully this just won't be an issue. ## Codemod This PR includes a codemod that automatically inserts the above mentioned v3 reset to help avoid breaking changes during the upgrade. The codemod will insert the following CSS: ```css /* In Tailwind CSS v4, basic styles are applied to form elements by default. To maintain compatibility with v3, the following resets have been added: */ @layer base { input, textarea, select, button { border: 0px solid; border-radius: 0; padding: 0; background-color: transparent; } } ``` ## Testing These changes have been tested across a wide range of browsers, including Chrome, Safari, Firefox, Edge, and Opera on macOS and Windows, as well as Safari, Chrome, Firefox, and several lesser-known browsers on iOS and Android. However, some quirks still exist in certain mobile browsers, such as iOS Safari, which adds too much bottom padding below date and time inputs: Screenshot 2024-11-20 at 3 57 20 PM The only reliable way to address these issues is by applying `appearance: none` to these form controls. However, this felt too opinionated for Preflight, so we’ve opted to leave such adjustments to user-land implementations. --------- Co-authored-by: Jonathan Reinink Co-authored-by: Jordan Pittman --- CHANGELOG.md | 2 + integrations/upgrade/index.test.ts | 285 ++++++++++++++++ integrations/upgrade/js-config.test.ts | 256 +++++++++++++++ .../src/__snapshots__/index.test.ts.snap | 188 ++++++++--- .../src/codemods/migrate-forms-reset.test.ts | 305 ++++++++++++++++++ .../src/codemods/migrate-forms-reset.ts | 49 +++ .../@tailwindcss-upgrade/src/index.test.ts | 45 +++ packages/@tailwindcss-upgrade/src/migrate.ts | 2 + packages/tailwindcss/preflight.css | 283 +++++++++++----- 9 files changed, 1276 insertions(+), 139 deletions(-) create mode 100644 packages/@tailwindcss-upgrade/src/codemods/migrate-forms-reset.test.ts create mode 100644 packages/@tailwindcss-upgrade/src/codemods/migrate-forms-reset.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 247fe492c..c8e0736f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add consistent base styles for buttons and form controls ([#15036](https://github.com/tailwindlabs/tailwindcss/pull/15036)) - _Upgrade (experimental)_: Convert `group-[]:flex` to `in-[.group]:flex` ([#15054](https://github.com/tailwindlabs/tailwindcss/pull/15054)) +- _Upgrade (experimental)_: Add form reset styles to CSS files for compatibility with v3 ([#15036](https://github.com/tailwindlabs/tailwindcss/pull/15036)) ### Fixed diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index f4dbb2ad2..1d91788d3 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -131,6 +131,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } @utility foo { color: red; @@ -223,6 +238,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } .btn { @apply tw:rounded-md! tw:px-2 tw:py-1 tw:bg-blue-500 tw:text-white; @@ -287,6 +317,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } .a { @apply flex; @@ -359,6 +404,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } @layer base { html { @@ -436,6 +496,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } @utility btn { @apply rounded-md px-2 py-1 bg-blue-500 text-white; @@ -545,6 +620,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } --- ./src/base.css --- html { @@ -1049,6 +1139,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } --- ./src/utilities.css --- @utility no-scrollbar { @@ -1497,6 +1602,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } --- ./src/root.2/index.css --- /* Already contains @config */ @@ -1521,6 +1641,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } --- ./src/root.3/index.css --- /* Inject missing @config above first @theme */ @@ -1555,6 +1690,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } --- ./src/root.4/index.css --- /* Inject missing @config due to nested imports with tailwind imports */ @@ -1584,6 +1734,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } --- ./src/root.4/utilities.css --- @import 'tailwindcss/utilities' layer(utilities); @@ -1615,6 +1780,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } " `) }, @@ -1795,6 +1975,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } @layer base { html { @@ -1931,6 +2126,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } @layer base { html { @@ -2048,6 +2258,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } @layer base { html { @@ -2123,6 +2348,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } --- ./src/styles/components.css --- .btn { @@ -2340,6 +2580,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } --- index.html ---
@@ -2422,6 +2677,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } --- index.html ---
@@ -2534,6 +2804,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } --- ./src/components.css --- @import './typography.css'; diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index e0173bcf3..e683986d1 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -284,6 +284,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } --- src/test.js --- export default { @@ -393,6 +408,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } " `) @@ -475,6 +505,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } " `) @@ -549,6 +594,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } " `) @@ -627,6 +687,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } " `) @@ -701,6 +776,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } " `) @@ -813,6 +903,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } --- project-b/src/input.css --- @import 'tailwindcss'; @@ -838,6 +943,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } " `) }, @@ -915,6 +1035,21 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } " `) }, @@ -975,6 +1110,21 @@ describe('border compatibility', () => { border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } " `) }, @@ -1037,6 +1187,21 @@ describe('border compatibility', () => { border-color: oklch(0.623 0.214 259.815); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } " `) }, @@ -1081,6 +1246,22 @@ describe('border compatibility', () => { " --- src/input.css --- @import 'tailwindcss'; + + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } " `) }, @@ -1141,6 +1322,21 @@ describe('border compatibility', () => { border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } " `) }, @@ -1210,6 +1406,21 @@ describe('border compatibility', () => { border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } --- src/input.css --- @import './base.css'; @@ -1328,6 +1539,21 @@ describe('border compatibility', () => { border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } .container { width: calc(var(--spacing) * 2); @@ -1442,6 +1668,21 @@ describe('border compatibility', () => { border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } .container { width: var(--spacing-2); @@ -1545,6 +1786,21 @@ describe('border compatibility', () => { border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } " `) }, diff --git a/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap b/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap index 17c977f96..263e01037 100644 --- a/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap +++ b/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap @@ -393,6 +393,11 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` font-feature-settings: var(--default-font-feature-settings, normal); font-variation-settings: var(--default-font-variation-settings, normal); -webkit-tap-highlight-color: transparent; + --lightningcss-light: initial; + --lightningcss-dark: ; + --lightningcss-light: initial; + --lightningcss-dark: ; + color-scheme: light; } body { @@ -459,50 +464,55 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` border-collapse: collapse; } - button, input, optgroup, select, textarea { - font: inherit; - font-feature-settings: inherit; - font-variation-settings: inherit; - letter-spacing: inherit; - color: inherit; - } - - ::file-selector-button { - font: inherit; - font-feature-settings: inherit; - font-variation-settings: inherit; - letter-spacing: inherit; - color: inherit; - } - - button, input:where([type="button"], [type="reset"], [type="submit"]) { - appearance: button; - background: none; - } - - ::file-selector-button { - appearance: button; - background: none; - } - :-moz-focusring { outline: auto; } - :-moz-ui-invalid { - box-shadow: none; - } - progress { vertical-align: baseline; } - ::-webkit-inner-spin-button { + summary { + display: list-item; + } + + ol, ul, menu { + list-style: none; + } + + img, svg, video, canvas, audio, iframe, embed, object { + vertical-align: middle; + display: block; + } + + img, video { + max-width: 100%; height: auto; } - ::-webkit-outer-spin-button { - height: auto; + button, input, select, optgroup, textarea { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + } + + ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + } + + ::placeholder { + opacity: 1; + color: color-mix(in oklch, currentColor 50%, transparent); + } + + textarea { + resize: vertical; } ::-webkit-search-decoration { @@ -558,33 +568,109 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` padding-block: 0; } - summary { - display: list-item; + :-moz-ui-invalid { + box-shadow: none; } - ol, ul, menu { - list-style: none; + button, input:where([type="button"], [type="reset"], [type="submit"]) { + appearance: button; } - textarea { - resize: vertical; + ::file-selector-button { + appearance: button; } - ::placeholder { - opacity: 1; - color: color-mix(in oklch, currentColor 50%, transparent); - } - - img, svg, video, canvas, audio, iframe, embed, object { - vertical-align: middle; - display: block; - } - - img, video { - max-width: 100%; + ::-webkit-inner-spin-button { height: auto; } + ::-webkit-outer-spin-button { + height: auto; + } + + select, textarea, input:where([type="text"], [type="email"], [type="password"], [type="date"], [type="datetime-local"], [type="month"], [type="number"], [type="search"], [type="time"], [type="week"], [type="tel"], [type="url"]) { + color: var(--lightningcss-light, #000) var(--lightningcss-dark, #fff); + background-color: var(--lightningcss-light, #fff) var(--lightningcss-dark, #ffffff1a); + border: 1px solid var(--lightningcss-light, #00000080) var(--lightningcss-dark, #ffffff40); + border-radius: 3px; + padding-block: 2px; + padding-inline: 4px; + } + + :where(select:not([multiple], [size])) option, :where(select:not([multiple], [size])) optgroup { + color: fieldtext; + background-color: field; + } + + :where(select:is([multiple], [size])) optgroup { + font-weight: bold; + } + + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + + select:where(:disabled), textarea:where(:disabled), input:where([type="text"], [type="email"], [type="password"], [type="date"], [type="datetime-local"], [type="month"], [type="number"], [type="search"], [type="time"], [type="week"], [type="tel"], [type="url"]):where(:disabled) { + opacity: 1; + color: var(--lightningcss-light, #00000080) var(--lightningcss-dark, #ffffff80); + background-color: var(--lightningcss-light, #00000006) var(--lightningcss-dark, #ffffff1a); + border-color: var(--lightningcss-light, #0003) var(--lightningcss-dark, #ffffff26); + } + + button, input:where([type="button"], [type="reset"], [type="submit"]) { + color: var(--lightningcss-light, #000) var(--lightningcss-dark, #fff); + background-color: var(--lightningcss-light, #0000000d) var(--lightningcss-dark, #fff6); + border: 1px solid var(--lightningcss-light, #00000080) var(--lightningcss-dark, #ffffff1a); + border-radius: 4px; + padding-block: 2px; + padding-inline: 4px; + } + + ::file-selector-button { + color: var(--lightningcss-light, #000) var(--lightningcss-dark, #fff); + background-color: var(--lightningcss-light, #0000000d) var(--lightningcss-dark, #fff6); + border: 1px solid var(--lightningcss-light, #00000080) var(--lightningcss-dark, #ffffff1a); + border-radius: 4px; + padding-block: 2px; + padding-inline: 4px; + } + + button:where(:hover), input:where([type="button"], [type="reset"], [type="submit"]):where(:hover) { + background-color: var(--lightningcss-light, #0000001a) var(--lightningcss-dark, #ffffff73); + } + + ::file-selector-button:where(:hover) { + background-color: var(--lightningcss-light, #0000001a) var(--lightningcss-dark, #ffffff73); + } + + button:where(:active), input:where([type="button"], [type="reset"], [type="submit"]):where(:active) { + background-color: var(--lightningcss-light, #00000006) var(--lightningcss-dark, #ffffff4d); + } + + ::file-selector-button:where(:active) { + background-color: var(--lightningcss-light, #00000006) var(--lightningcss-dark, #ffffff4d); + } + + button:where(:disabled), input:where([type="button"], [type="reset"], [type="submit"]):where(:disabled) { + opacity: 1; + background-color: var(--lightningcss-light, #00000006) var(--lightningcss-dark, #ffffff40); + border-color: var(--lightningcss-light, #0003) var(--lightningcss-dark, #ffffff1a); + } + + :where(input:disabled)::file-selector-button { + opacity: 1; + background-color: var(--lightningcss-light, #00000006) var(--lightningcss-dark, #ffffff40); + border-color: var(--lightningcss-light, #0003) var(--lightningcss-dark, #ffffff1a); + } + + input:where([type="file"]:disabled) { + color: var(--lightningcss-light, #00000080) var(--lightningcss-dark, #ffffff80); + } + + ::file-selector-button { + margin-inline-end: 4px; + } + [hidden]:where(:not([hidden="until-found"])) { display: none !important; } diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-forms-reset.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-forms-reset.test.ts new file mode 100644 index 000000000..da7118ab2 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-forms-reset.test.ts @@ -0,0 +1,305 @@ +import dedent from 'dedent' +import postcss from 'postcss' +import { expect, it } from 'vitest' +import { formatNodes } from './format-nodes' +import { migrateFormsReset } from './migrate-forms-reset' +import { sortBuckets } from './sort-buckets' + +const css = dedent + +async function migrate(input: string) { + return postcss() + .use(migrateFormsReset()) + .use(sortBuckets()) + .use(formatNodes()) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) +} + +it("should add compatibility CSS after the `@import 'tailwindcss'`", async () => { + expect( + await migrate(css` + @import 'tailwindcss'; + `), + ).toMatchInlineSnapshot(` + "@import 'tailwindcss'; + + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + }" + `) +}) + +it('should add the compatibility CSS after the last `@import`', async () => { + expect( + await migrate(css` + @import 'tailwindcss'; + @import './foo.css'; + @import './bar.css'; + `), + ).toMatchInlineSnapshot(` + "@import 'tailwindcss'; + @import './foo.css'; + @import './bar.css'; + + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + }" + `) +}) + +it('should add the compatibility CSS after the last import, even if a body-less `@layer` exists', async () => { + expect( + await migrate(css` + @charset "UTF-8"; + @layer foo, bar, baz, base; + + /**! + * License header + */ + + @import 'tailwindcss'; + @import './foo.css'; + @import './bar.css'; + `), + ).toMatchInlineSnapshot(` + "@charset "UTF-8"; + @layer foo, bar, baz, base; + + /**! + * License header + */ + + @import 'tailwindcss'; + @import './foo.css'; + @import './bar.css'; + + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + }" + `) +}) + +it('should add the compatibility CSS before the first `@layer base` (if the "tailwindcss" import exists)', async () => { + expect( + await migrate(css` + @import 'tailwindcss'; + + @variant foo { + } + + @utility bar { + } + + @layer base { + } + + @utility baz { + } + + @layer base { + } + `), + ).toMatchInlineSnapshot(` + "@import 'tailwindcss'; + + @variant foo { + } + + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } + + @utility bar { + } + + @utility baz { + } + + @layer base { + } + + @layer base { + }" + `) +}) + +it('should add the compatibility CSS before the first `@layer base` (if the "tailwindcss/preflight" import exists)', async () => { + expect( + await migrate(css` + @import 'tailwindcss/preflight'; + + @variant foo { + } + + @utility bar { + } + + @layer base { + } + + @utility baz { + } + + @layer base { + } + `), + ).toMatchInlineSnapshot(` + "@import 'tailwindcss/preflight'; + + @variant foo { + } + + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } + + @utility bar { + } + + @utility baz { + } + + @layer base { + } + + @layer base { + }" + `) +}) + +it('should not add the backwards compatibility CSS when no `@import "tailwindcss"` or `@import "tailwindcss/preflight"` exists', async () => { + expect( + await migrate(css` + @variant foo { + } + + @utility bar { + } + + @layer base { + } + + @utility baz { + } + + @layer base { + } + `), + ).toMatchInlineSnapshot(` + "@variant foo { + } + + @utility bar { + } + + @utility baz { + } + + @layer base { + } + + @layer base { + }" + `) +}) + +it('should not add the backwards compatibility CSS when another `@import "tailwindcss"` import exists such as theme or utilities', async () => { + expect( + await migrate(css` + @import 'tailwindcss/theme'; + + @variant foo { + } + + @utility bar { + } + + @layer base { + } + + @utility baz { + } + + @layer base { + } + `), + ).toMatchInlineSnapshot(` + "@import 'tailwindcss/theme'; + + @variant foo { + } + + @utility bar { + } + + @utility baz { + } + + @layer base { + } + + @layer base { + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-forms-reset.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-forms-reset.ts new file mode 100644 index 000000000..875095318 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-forms-reset.ts @@ -0,0 +1,49 @@ +import dedent from 'dedent' +import postcss, { type Plugin, type Root } from 'postcss' + +const css = dedent + +const FORMS_RESET_CSS = css` + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } +` + +export function migrateFormsReset(): Plugin { + function migrate(root: Root) { + let isTailwindRoot = false + root.walkAtRules('import', (node) => { + if ( + /['"]tailwindcss['"]/.test(node.params) || + /['"]tailwindcss\/preflight['"]/.test(node.params) + ) { + isTailwindRoot = true + return false + } + }) + + if (!isTailwindRoot) return + + let compatibilityCssString = `\n@tw-bucket compatibility {\n${FORMS_RESET_CSS}\n}\n` + let compatibilityCss = postcss.parse(compatibilityCssString) + + root.append(compatibilityCss) + } + + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-forms-reset', + OnceExit: migrate, + } +} diff --git a/packages/@tailwindcss-upgrade/src/index.test.ts b/packages/@tailwindcss-upgrade/src/index.test.ts index 65d74937b..138148071 100644 --- a/packages/@tailwindcss-upgrade/src/index.test.ts +++ b/packages/@tailwindcss-upgrade/src/index.test.ts @@ -113,6 +113,21 @@ it('should migrate a stylesheet', async () => { border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } @utility b { z-index: 2; @@ -183,6 +198,21 @@ it('should migrate a stylesheet (with imports)', async () => { ::file-selector-button { border-color: var(--color-gray-200, currentColor); } + } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } }" `) }) @@ -226,6 +256,21 @@ it('should migrate a stylesheet (with preceding rules that should be wrapped in border-color: var(--color-gray-200, currentColor); } } + /* + In Tailwind CSS v4, basic styles are applied to form elements by default. To + maintain compatibility with v3, the following resets have been added: + */ + @layer base { + input, + textarea, + select, + button { + border: 0px solid; + border-radius: 0; + padding: 0; + background-color: transparent; + } + } @layer base { html { diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts index 1e6af335a..7ddca0e52 100644 --- a/packages/@tailwindcss-upgrade/src/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/migrate.ts @@ -10,6 +10,7 @@ import { migrateAtApply } from './codemods/migrate-at-apply' import { migrateAtLayerUtilities } from './codemods/migrate-at-layer-utilities' import { migrateBorderCompatibility } from './codemods/migrate-border-compatibility' import { migrateConfig } from './codemods/migrate-config' +import { migrateFormsReset } from './codemods/migrate-forms-reset' import { migrateImport } from './codemods/migrate-import' import { migrateMediaScreen } from './codemods/migrate-media-screen' import { migrateMissingLayers } from './codemods/migrate-missing-layers' @@ -51,6 +52,7 @@ export async function migrateContents( .use(migrateTailwindDirectives(options)) .use(migrateConfig(stylesheet, options)) .use(migrateBorderCompatibility(options)) + .use(migrateFormsReset()) .use(migrateThemeToVar(options)) .process(stylesheet.root, { from: stylesheet.file ?? undefined }) } diff --git a/packages/tailwindcss/preflight.css b/packages/tailwindcss/preflight.css index 6b8cbbae4..7ccadd35d 100644 --- a/packages/tailwindcss/preflight.css +++ b/packages/tailwindcss/preflight.css @@ -23,6 +23,7 @@ 5. Use the user's configured `sans` font-feature-settings by default. 6. Use the user's configured `sans` font-variation-settings by default. 7. Disable tap highlights on iOS. + 8. Set a default `color-scheme`. */ html, @@ -43,6 +44,7 @@ html, font-feature-settings: var(--default-font-feature-settings, normal); /* 5 */ font-variation-settings: var(--default-font-variation-settings, normal); /* 6 */ -webkit-tap-highlight-color: transparent; /* 7 */ + color-scheme: light; /* 8 */ } /* @@ -174,35 +176,6 @@ table { border-collapse: collapse; /* 3 */ } -/* - Inherit font styles in all browsers. -*/ - -button, -input, -optgroup, -select, -textarea, -::file-selector-button { - font: inherit; - font-feature-settings: inherit; - font-variation-settings: inherit; - letter-spacing: inherit; - color: inherit; -} - -/* - 1. Remove the default background color of buttons by default. - 2. Correct the inability to style the border radius in iOS Safari. -*/ - -button, -input:where([type='button'], [type='reset'], [type='submit']), -::file-selector-button { - background: transparent; /* 1 */ - appearance: button; /* 2 */ -} - /* Use the modern Firefox focus style for all focusable elements. */ @@ -211,14 +184,6 @@ input:where([type='button'], [type='reset'], [type='submit']), outline: auto; } -/* - Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) -*/ - -:-moz-ui-invalid { - box-shadow: none; -} - /* Add the correct vertical alignment in Chrome and Firefox. */ @@ -228,14 +193,86 @@ progress { } /* - Correct the cursor style of increment and decrement buttons in Safari. + Add the correct display in Chrome and Safari. */ -::-webkit-inner-spin-button, -::-webkit-outer-spin-button { +summary { + display: list-item; +} + +/* + Make lists unstyled by default. +*/ + +ol, +ul, +menu { + list-style: none; +} + +/* + 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) + 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; /* 1 */ + vertical-align: middle; /* 2 */ +} + +/* + Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; height: auto; } +/* + Inherit font styles in all browsers. +*/ + +button, +input, +select, +optgroup, +textarea, +::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; +} + +/* + 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) + 2. Set the default placeholder color to a semi-transparent version of the current text color. +*/ + +::placeholder { + opacity: 1; /* 1 */ + color: color-mix(in oklch, currentColor 50%, transparent); /* 2 */ +} + +/* + Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + /* Remove the inner padding in Chrome and Safari on macOS. */ @@ -283,69 +320,139 @@ progress { } /* - Add the correct display in Chrome and Safari. + Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) */ -summary { - display: list-item; +:-moz-ui-invalid { + box-shadow: none; } /* - Make lists unstyled by default. + Correct the inability to style the border radius in iOS Safari. */ -ol, -ul, -menu { - list-style: none; +button, +input:where([type='button'], [type='reset'], [type='submit']), +::file-selector-button { + appearance: button; } /* - Prevent resizing textareas horizontally by default. + Correct the cursor style of increment and decrement buttons in Safari. */ -textarea { - resize: vertical; -} - -/* - 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) - 2. Set the default placeholder color to a semi-transparent version of the current text color. -*/ - -::placeholder { - opacity: 1; /* 1 */ - color: color-mix(in oklch, currentColor 50%, transparent); /* 2 */ -} - -/* - 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) - 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) - This can trigger a poorly considered lint error in some tools but is included by design. -*/ - -img, -svg, -video, -canvas, -audio, -iframe, -embed, -object { - display: block; /* 1 */ - vertical-align: middle; /* 2 */ -} - -/* - Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) -*/ - -img, -video { - max-width: 100%; +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { height: auto; } +/* + Apply consistent base form control styles across browsers. +*/ + +select, +textarea, +input:where( + [type='text'], + [type='email'], + [type='password'], + [type='date'], + [type='datetime-local'], + [type='month'], + [type='number'], + [type='search'], + [type='time'], + [type='week'], + [type='tel'], + [type='url'] + ) { + border-radius: 3px; + padding-inline: 4px; + padding-block: 2px; + color: light-dark(black, white); + background-color: light-dark(white, rgb(255 255 255 / 10%)); + border: 1px solid light-dark(rgb(0 0 0 / 50%), rgb(255 255 255 / 25%)); +} + +:where(select:not([multiple], [size])) option, +:where(select:not([multiple], [size])) optgroup { + color: FieldText; + background-color: Field; +} + +:where(select:is([multiple], [size])) optgroup { + font-weight: bold; +} + +:where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; +} + +select:where(:disabled), +textarea:where(:disabled), +input:where( + [type='text'], + [type='email'], + [type='password'], + [type='date'], + [type='datetime-local'], + [type='month'], + [type='number'], + [type='search'], + [type='time'], + [type='week'], + [type='tel'], + [type='url'] + ):where(:disabled) { + opacity: 1; + color: light-dark(rgb(0 0 0 / 50%), rgb(255 255 255 / 50%)); + background-color: light-dark(rgb(0 0 0 / 2.5%), rgb(255 255 255 / 10%)); + border-color: light-dark(rgb(0 0 0 / 20%), rgb(255 255 255 / 15%)); +} + +/* + Apply consistent base button styles across browsers. +*/ + +button, +::file-selector-button, +input:where([type='button'], [type='reset'], [type='submit']) { + border-radius: 4px; + padding-inline: 4px; + padding-block: 2px; + color: light-dark(#000, #fff); + background-color: light-dark(rgb(0 0 0 / 5%), rgb(255 255 255 / 40%)); + border: 1px solid light-dark(rgb(0 0 0 / 50%), rgb(255 255 255 / 10%)); +} + +button:where(:hover), +::file-selector-button:where(:hover), +input:where([type='button'], [type='reset'], [type='submit']):where(:hover) { + background-color: light-dark(rgb(0 0 0 / 10%), rgb(255 255 255 / 45%)); +} + +button:where(:active), +::file-selector-button:where(:active), +input:where([type='button'], [type='reset'], [type='submit']):where(:active) { + background-color: light-dark(rgb(0 0 0 / 2.5%), rgb(255 255 255 / 30%)); +} + +button:where(:disabled), +:where(input:disabled)::file-selector-button, +input:where([type='button'], [type='reset'], [type='submit']):where(:disabled) { + opacity: 1; + background-color: light-dark(rgb(0 0 0 / 2.5%), rgb(255 255 255 / 25%)); + border-color: light-dark(rgb(0 0 0 / 20%), rgb(255 255 255 / 10%)); +} + +input:where([type='file']:disabled) { + color: light-dark(rgb(0 0 0 / 50%), rgb(255 255 255 / 50%)); +} + +::file-selector-button { + margin-inline-end: 4px; +} + /* Make elements with the HTML hidden attribute stay hidden by default. */