Repository navigation
Improve compatibility with Safari 15 - #17435
Conversation
cb15f4e to
521c5ed
Compare
RobinMalfait
left a comment
There was a problem hiding this comment.
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 (') |
| .text-red-500\\/\\(--my-half\\) { | ||
| color: #fb2c3680; | ||
| } |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
I wonder if we should store the polyfills on the designSystem itself 🤔
…computation is just too flaky across OSes
…included in IntelliSense output
2ded17c to
9244037
Compare
|
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 ( |
|
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? |
|
@philipp-spiess , any help regarding this? |
|
@sehmbimanvir Do you have a repro please? I suppose this should work: https://play.tailwindcss.com/aqmzubxchz The play generates a |
|
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? |
|
@sehmbimanvir Please attach a full reproduction here, the variables are put in an |
/** 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. |
Yeah this is intentional. Your CSS file and HTML seems to work fine for me in iOS 16.4 Safari, see:
|
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

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
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).@propertywith a@supportsquery targeting older versions of Safari and Firefox *color-mix(…)function that use inlined color values from your theme so that they can be computed a compile time bylightningcss. These fallbacks will convert to srgb to increase compatibility.color-mix(…)in case relative color is applied oncurrentcolor(due to limited browser support)bg-linear-to-r/oklab)@mediaqueries range syntax.A simplified example
Given this example CSS input:
Here's the updated output CSS including the newly added polyfills and updated
oklabvalues:* A note on
@propertypolyfills and CSS modulesOn 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
@propertyrules which we've been using before to initialize CSS variables.However, since we're now bringing back the
@propertypolyfills, that would cause unexpected rules to be exported from the CSS file as this:Would turn to the following file:
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
@propertypolyfill not require an@layer baseand instead place the generated CSS on the top of the file?@mediarange query workaroundTest plan