镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Improve compatibility with Safari 15 - #17435

Merged
philipp-spiess merged 28 commits into
mainfrom
fix/safari-15
Apr 1, 2025
Merged

philipp-spiess merged 28 commits into
mainfrom
fix/safari-15

Conversation

@philipp-spiess

@philipp-spiess philipp-spiess commented Mar 28, 2025 •

Copy link
Copy Markdown
Contributor

This PR improves the compatibility with Tailwind CSS v4 with unsupported browsers with the goal to greatly improve compatibility with Safari 15.

To make this work, this PR makes the following changes to all code

  • Change oklab(…) default theme values to use a percentage in the first place (so instead of --color-red-500: oklch(0.637 0.237 25.331); we now define it as --color-red-500: oklch(63.7% 0.237 25.331); since this syntax has much broader support on Safari).
  • Polyfill @property with a @supports query targeting older versions of Safari and Firefox *
  • Create fallbacks for the color-mix(…) function that use inlined color values from your theme so that they can be computed a compile time by lightningcss. These fallbacks will convert to srgb to increase compatibility.
  • Create fallbacks for the relative color feature used in the new shadow utilities and using color-mix(…) in case relative color is applied on currentcolor (due to limited browser support)
  • Create fallbacks for gradient interpolation methods (e.g. to support bg-linear-to-r/oklab)
  • Polyfill @media queries range syntax.

A simplified example

Given this example CSS input:

@import 'tailwindcss';
@source inline('from-cyan-500/50 bg-linear-45');

Here's the updated output CSS including the newly added polyfills and updated oklab values:

.bg-linear-45 {
  --tw-gradient-position: 45deg;
  background-image: linear-gradient(var(--tw-gradient-stops));
}

@supports (background-image: linear-gradient(in lab, red, red)) {
  .bg-linear-45 {
    --tw-gradient-position: 45deg in oklab;
  }
}

.from-cyan-500\\/50 {
  --tw-gradient-from: oklab(71.5% -.11682 -.08247 / .5);
  --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}

@supports (color: color-mix(in lab, red, red)) {
  .from-cyan-500\\/50 {
    --tw-gradient-from: color-mix(in oklab, var(--color-cyan-500) 50%, transparent);
  }
}

:root, :host {
  --color-cyan-500: oklch(71.5% .143 215.221);
}

@supports (((-webkit-hyphens: none)) and (not (margin-trim: 1lh))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
  @layer base {
    *, :before, :after, ::backdrop {
      --tw-gradient-position: initial;
      --tw-gradient-from: #0000;
      --tw-gradient-via: #0000;
      --tw-gradient-to: #0000;
      --tw-gradient-stops: initial;
      --tw-gradient-via-stops: initial;
      --tw-gradient-from-position: 0%;
      --tw-gradient-via-position: 50%;
      --tw-gradient-to-position: 100%;
    }
  }
}

@property --tw-gradient-position {
  syntax: "*";
  inherits: false
}

@property --tw-gradient-from {
  syntax: "<color>";
  inherits: false;
  initial-value: #0000;
}

@property --tw-gradient-via {
  syntax: "<color>";
  inherits: false;
  initial-value: #0000;
}

@property --tw-gradient-to {
  syntax: "<color>";
  inherits: false;
  initial-value: #0000;
}

@property --tw-gradient-stops {
  syntax: "*";
  inherits: false
}

@property --tw-gradient-via-stops {
  syntax: "*";
  inherits: false
}

@property --tw-gradient-from-position {
  syntax: "<length-percentage>";
  inherits: false;
  initial-value: 0%;
}

@property --tw-gradient-via-position {
  syntax: "<length-percentage>";
  inherits: false;
  initial-value: 50%;
}

@property --tw-gradient-to-position {
  syntax: "<length-percentage>";
  inherits: false;
  initial-value: 100%;
}

* A note on @property polyfills and CSS modules

On Next.js, CSS module files are required to be pure, meaning that all selectors must either be scoped to a class or an ID. Fortunatnyl for us, this does not apply to @property rules which we've been using before to initialize CSS variables.

However, since we're now bringing back the @property polyfills, that would cause unexpected rules to be exported from the CSS file as this:

@reference "tailwindcss";

.skew {
  @apply skew-7;
}

Would turn to the following file:

.skew {
  /* … */
}
@supports (/*…*/) {
  @layer base {
    *, :before, :after, ::backdrop {
      --tw-gradient-position: initial;
    }
  }
}
@property /* … */ 

Notice that this adds a * selector which is not considered pure.

Unfortunately there is no way for us to silence this warning or work around it, as the dependency causing this errors (postcss-modules-local-by-default) is bundled into Next.js. To work around crashes, these polyfills will not apply to CSS modules processed by the PostCSS extension for now.

Testing on tailwindcss.com

To see the changes in effect, take a look at this screencast that compares tailwindcss.com on iOS 15.5 with a version that has the patches of this PR applied:

Screen.Recording.2025-03-28.at.17.52.25.mov

Open questions

  • Should we make the @property polyfill not require an @layer base and instead place the generated CSS on the top of the file?
  • Add fallbacks for newly introduced usages of the relative color syntax.
  • Decide if we're ok with how we actually implement the @media range query workaround

Test plan

@philipp-spiess
philipp-spiess force-pushed the fix/safari-15 branch 3 times, most recently from cb15f4e to 521c5ed Compare March 31, 2025 12:01
@philipp-spiess
philipp-spiess marked this pull request as ready for review March 31, 2025 14:18
@philipp-spiess
philipp-spiess requested a review from a team as a code owner March 31, 2025 14:18

@RobinMalfait RobinMalfait left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't functionally tested yet, but wanted to leave these notes already.


// Work around an issue where the media query range syntax transpilation
// generates code that is invalid with `@media` queries level 3.
out = out.replaceAll('@media not (', '@media not all and (')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😅

Comment thread packages/tailwindcss/src/ast.ts Resolved
Comment thread packages/tailwindcss/src/ast.ts Resolved
Comment on lines +4932 to +4934
.text-red-500\\/\\(--my-half\\) {
color: #fb2c3680;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sad that this means that you can't change the --my-half value at runtime, but if it's not supported what can you do haha.


if (!utilitiesNode) {
compiled ??= optimizeAst(ast, designSystem)
compiled ??= optimizeAst(ast, designSystem, opts.polyfills)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should store the polyfills on the designSystem itself 🤔

Comment thread playgrounds/nextjs/app/page.module.css Resolved
Comment thread packages/tailwindcss/src/ast.ts Resolved
Comment thread packages/tailwindcss/src/ast.ts Resolved
Comment thread packages/tailwindcss/src/utilities.ts Resolved
Comment thread packages/tailwindcss/src/index.test.ts Resolved
@silvenon

silvenon commented Apr 6, 2025 •

Copy link
Copy Markdown

Sorry for the late comment, but it is my understanding that with this PR merged the general Safari support for basic Tailwind v4 goes from 16.4 to 16.2 (color-mix is supported down to 16.2). Is that true?

@sehmbimanvir

sehmbimanvir commented May 17, 2025 •

Copy link
Copy Markdown

I am facing issue with gradient if I defined a color in rgb format

e.g

// index.css

@theme inline {
    --color-custom: rgba(111, 160, 0);
}

Now If, I am using the class for gradient with opacity e.g

<div class="bg-linear-to-tr from-0% to-40% from-custom/20" />

It's not working in iOS 16. Do, I need to change color to hex?

@sehmbimanvir

Copy link
Copy Markdown

@philipp-spiess , any help regarding this?

#17435 (comment)

@philipp-spiess

Copy link
Copy Markdown
Contributor Author

@sehmbimanvir Do you have a repro please? I suppose this should work: https://play.tailwindcss.com/aqmzubxchz The play generates a color-mix(…) function BUT in a production build, lightningcss is able to transpile it away.

@sehmbimanvir

Copy link
Copy Markdown

Gradient, blur isn't working for a production build. I tried to extract using tailwind cli tool.

Here are the results.

/* This is at line number 3000 */
@layer properties {
  @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
    *, ::before, ::after, ::backdrop {
      --tw-translate-x: 0;
      --tw-translate-y: 0;
      --tw-translate-z: 0;
      --tw-scale-x: 1;
      --tw-scale-y: 1;
      --tw-scale-z: 1;
      --tw-rotate-x: initial;
      --tw-rotate-y: initial;
      --tw-rotate-z: initial;
      --tw-skew-x: initial;
      --tw-skew-y: initial;
       // more variables
/* This is at line number 1600 */
.from-background-primary {
    --tw-gradient-from: var(--bg-primary);
    --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
  }

All variables are getting generated after the actual class. So, these initial values are replacing the property being used in class. @philipp-spiess, Any help or fix for this?

@philipp-spiess

Copy link
Copy Markdown
Contributor Author

@sehmbimanvir Please attach a full reproduction here, the variables are put in an @layer properties so they should still come first.

@sehmbimanvir

Copy link
Copy Markdown

@sehmbimanvir Please attach a full reproduction here, the variables are put in an @layer properties so they should still come first.

@philipp-spiess ,

/** App.css */
@import 'tailwindcss';


@theme {
  --color-custom-100: #e67e22;
}
<div class='h-screen w-screen flex justify-center items-center'>
      <div class="flex items-center justify-center h-60 w-60 rounded-xl bg-linear-to-r from-custom-100 to-red-100">Hello</div>
    </div>

Please find the below CSS generated by tailwind cli.

/*! tailwindcss v4.1.7 | MIT License | https://tailwindcss.com */
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
  :root, :host {
    --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
      "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
    --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
      "Courier New", monospace;
    --color-red-100: oklch(93.6% 0.032 17.717);
    --spacing: 0.25rem;
    --radius-xl: 0.75rem;
    --default-font-family: var(--font-sans);
    --default-mono-font-family: var(--font-mono);
    --color-custom-100: #e67e22;
  }
}
@layer base {
  *, ::after, ::before, ::backdrop, ::file-selector-button {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    border: 0 solid;
  }
  html, :host {
    line-height: 1.5;
    -webkit-text-size-adjust: 100%;
    tab-size: 4;
    font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
    font-feature-settings: var(--default-font-feature-settings, normal);
    font-variation-settings: var(--default-font-variation-settings, normal);
    -webkit-tap-highlight-color: transparent;
  }
  hr {
    height: 0;
    color: inherit;
    border-top-width: 1px;
  }
  abbr:where([title]) {
    -webkit-text-decoration: underline dotted;
    text-decoration: underline dotted;
  }
  h1, h2, h3, h4, h5, h6 {
    font-size: inherit;
    font-weight: inherit;
  }
  a {
    color: inherit;
    -webkit-text-decoration: inherit;
    text-decoration: inherit;
  }
  b, strong {
    font-weight: bolder;
  }
  code, kbd, samp, pre {
    font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
    font-feature-settings: var(--default-mono-font-feature-settings, normal);
    font-variation-settings: var(--default-mono-font-variation-settings, normal);
    font-size: 1em;
  }
  small {
    font-size: 80%;
  }
  sub, sup {
    font-size: 75%;
    line-height: 0;
    position: relative;
    vertical-align: baseline;
  }
  sub {
    bottom: -0.25em;
  }
  sup {
    top: -0.5em;
  }
  table {
    text-indent: 0;
    border-color: inherit;
    border-collapse: collapse;
  }
  :-moz-focusring {
    outline: auto;
  }
  progress {
    vertical-align: baseline;
  }
  summary {
    display: list-item;
  }
  ol, ul, menu {
    list-style: none;
  }
  img, svg, video, canvas, audio, iframe, embed, object {
    display: block;
    vertical-align: middle;
  }
  img, video {
    max-width: 100%;
    height: auto;
  }
  button, input, select, optgroup, textarea, ::file-selector-button {
    font: inherit;
    font-feature-settings: inherit;
    font-variation-settings: inherit;
    letter-spacing: inherit;
    color: inherit;
    border-radius: 0;
    background-color: transparent;
    opacity: 1;
  }
  :where(select:is([multiple], [size])) optgroup {
    font-weight: bolder;
  }
  :where(select:is([multiple], [size])) optgroup option {
    padding-inline-start: 20px;
  }
  ::file-selector-button {
    margin-inline-end: 4px;
  }
  ::placeholder {
    opacity: 1;
  }
  @supports (not (-webkit-appearance: -apple-pay-button))  or (contain-intrinsic-size: 1px) {
    ::placeholder {
      color: currentcolor;
      @supports (color: color-mix(in lab, red, red)) {
        color: color-mix(in oklab, currentcolor 50%, transparent);
      }
    }
  }
  textarea {
    resize: vertical;
  }
  ::-webkit-search-decoration {
    -webkit-appearance: none;
  }
  ::-webkit-date-and-time-value {
    min-height: 1lh;
    text-align: inherit;
  }
  ::-webkit-datetime-edit {
    display: inline-flex;
  }
  ::-webkit-datetime-edit-fields-wrapper {
    padding: 0;
  }
  ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
    padding-block: 0;
  }
  :-moz-ui-invalid {
    box-shadow: none;
  }
  button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
    appearance: button;
  }
  ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
    height: auto;
  }
  [hidden]:where(:not([hidden="until-found"])) {
    display: none !important;
  }
}
@layer utilities {
  .flex {
    display: flex;
  }
  .h-60 {
    height: calc(var(--spacing) * 60);
  }
  .h-screen {
    height: 100vh;
  }
  .w-60 {
    width: calc(var(--spacing) * 60);
  }
  .w-screen {
    width: 100vw;
  }
  .items-center {
    align-items: center;
  }
  .justify-center {
    justify-content: center;
  }
  .rounded-xl {
    border-radius: var(--radius-xl);
  }
  .bg-linear-to-r {
    --tw-gradient-position: to right;
    @supports (background-image: linear-gradient(in lab, red, red)) {
      --tw-gradient-position: to right in oklab;
    }
    background-image: linear-gradient(var(--tw-gradient-stops));
  }
  .from-custom-100 {
    --tw-gradient-from: var(--color-custom-100);
    --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
  }
  .to-red-100 {
    --tw-gradient-to: var(--color-red-100);
    --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
  }
}
@property --tw-gradient-position {
  syntax: "*";
  inherits: false;
}
@property --tw-gradient-from {
  syntax: "<color>";
  inherits: false;
  initial-value: #0000;
}
@property --tw-gradient-via {
  syntax: "<color>";
  inherits: false;
  initial-value: #0000;
}
@property --tw-gradient-to {
  syntax: "<color>";
  inherits: false;
  initial-value: #0000;
}
@property --tw-gradient-stops {
  syntax: "*";
  inherits: false;
}
@property --tw-gradient-via-stops {
  syntax: "*";
  inherits: false;
}
@property --tw-gradient-from-position {
  syntax: "<length-percentage>";
  inherits: false;
  initial-value: 0%;
}
@property --tw-gradient-via-position {
  syntax: "<length-percentage>";
  inherits: false;
  initial-value: 50%;
}
@property --tw-gradient-to-position {
  syntax: "<length-percentage>";
  inherits: false;
  initial-value: 100%;
}
@layer properties {
  @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
    *, ::before, ::after, ::backdrop {
      --tw-gradient-position: initial;
      --tw-gradient-from: #0000;
      --tw-gradient-via: #0000;
      --tw-gradient-to: #0000;
      --tw-gradient-stops: initial;
      --tw-gradient-via-stops: initial;
      --tw-gradient-from-position: 0%;
      --tw-gradient-via-position: 50%;
      --tw-gradient-to-position: 100%;
    }
  }
}

You can see, one @layer properties is at top (which is empty) and other one is at bottom.

@philipp-spiess

philipp-spiess commented May 21, 2025 •

Copy link
Copy Markdown
Contributor Author

You can see, one https://github.com/layer properties is at top (which is empty) and other one is at bottom.

Yeah this is intentional. @layer properties with no children at the start will declare the layer as the first in the cascade, see: https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Cascade_layers#the_layer_statement_at-rule_for_named_layers

Your CSS file and HTML seems to work fine for me in iOS 16.4 Safari, see:

Screenshot 2025-05-21 at 10 31 41

suhaibbaba pushed a commit to suhaibbaba/organza-store that referenced this pull request Aug 21, 2026
Two follow-ups on the iOS 15 work.

Generate the fallbacks instead of relying on the bundler. Every colour is still
written once in oklch(); @csstools/postcss-oklab-function (preserve: true) now
derives the plain sRGB copy from it and keeps the original inside an @supports,
so the pair is produced from one source and cannot drift when a colour changes.
It also replaces an implicit behaviour with an explicit one: the fallbacks were
previously a side effect of Turbopack's own Lightning CSS pass reading
browserslist, which also rewrote the modern declaration to lab(). The modern
declaration is now the oklch() that was written.

The plugin cannot evaluate a colour whose hue arrives through a var() either —
it passes those through untouched, no fallback and no warning — which is the
same trap as before and the reason the brand hues stay literals. The palette
note and CLAUDE.md now say the fallback is derived and must never be typed.

Then the range above the fallback. Safari has known oklch() since 15.4, so from
15.4 up it is the modern declaration that paints, and nothing so far had looked
at it. Checking it turned up a sharper failure than the one being looked for:
Safari 15.x reads the LIGHTNESS as a percentage and discards the whole
declaration when it is a decimal — oklch(44% .06 206.4) paints,
oklch(0.44 .06 206.4) does not (tailwindlabs/tailwindcss#17435, reported again
in #18081 against 15.8). That is the worst shape available: @supports lets the
declaration in, the browser drops it anyway, and the fallback in front of it has
already been overwritten. The output satisfies it today only because a minifier
normalises the number, so check-css-target.js now fails the build on a decimal
lightness rather than trusting that.

The hue needs no unit: a bare number is what the spec calls for, what Safari 15
accepts and what Tailwind itself ships. A deg written at the source is
normalised away before it reaches the browser, so it would have documented a
protection that does not exist.

Verified by rendering the built stylesheet through two filters — iOS 15.0-15.3,
where the derived sRGB paints, and 15.4-15.8, where the native oklch() does and
the percentage-lightness bug is reproduced — across every screen swept before.
The two agree with each other and with a modern engine, and the painted RGB of
ten tokens is identical to the channel between them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Uhw3SZ2j9WT2GUHgdU2mR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants