From b328e0ee9a4745e71e07c8108d49ac01c00a7324 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 11 Jun 2026 14:01:46 +0200 Subject: [PATCH 1/5] feat(ios): support backgroundImage layering Add a separate backgroundImage layer on iOS, preserve backgroundColor compatibility, and document the layering behavior. --- .../ios/ui/Style/DecorationStyle.swift | 41 ++++++++++++++----- .../ios/ui/Style/StyleConverter.swift | 1 + .../ios-client/ios/ui/Views/VoltraChart.swift | 1 + .../ios/ui/Views/VoltraLinearGradient.swift | 2 +- packages/ios/src/styles/types.ts | 1 + website/docs/ios/development/styling.md | 9 ++-- 6 files changed, 40 insertions(+), 15 deletions(-) diff --git a/packages/ios-client/ios/ui/Style/DecorationStyle.swift b/packages/ios-client/ios/ui/Style/DecorationStyle.swift index 6b06b8d1..f4b9f41c 100644 --- a/packages/ios-client/ios/ui/Style/DecorationStyle.swift +++ b/packages/ios-client/ios/ui/Style/DecorationStyle.swift @@ -2,6 +2,7 @@ import SwiftUI struct DecorationStyle { var backgroundColor: BackgroundValue? + var backgroundImage: BackgroundValue? var cornerRadius: CGFloat? var border: (width: CGFloat, color: Color)? var shadow: (radius: CGFloat, color: Color, opacity: Double, offset: CGSize)? @@ -133,6 +134,14 @@ struct DecorationModifier: ViewModifier { return nil } + private var resolvedBackgroundImage: BackgroundValue? { + guard suppressesDecorativeContainerEffects, isFullBleedBackgroundCandidate else { + return style.backgroundImage + } + + return nil + } + private var resolvedGlassEffect: GlassEffect? { guard suppressesDecorativeContainerEffects else { return style.glassEffect @@ -143,18 +152,14 @@ struct DecorationModifier: ViewModifier { func body(content: Content) -> some View { content - .voltraIfLet(resolvedBackgroundColor) { content, bg in - switch bg { - case let .color(color): - content.background(color) - case let .linearGradient(gradient, start, end): - content.background(LinearGradient(gradient: gradient, startPoint: start, endPoint: end)) - case let .radialGradient(spec): - content.background { - radialGradientBackground(spec) + .background { + ZStack { + if let bg = resolvedBackgroundColor { + backgroundView(for: bg) + } + if let bg = resolvedBackgroundImage { + backgroundView(for: bg) } - case let .angularGradient(gradient, center, angle): - content.background(AngularGradient(gradient: gradient, center: center, angle: angle)) } } // If we have a corner radius, we must handle the border specifically here @@ -204,4 +209,18 @@ struct DecorationModifier: ViewModifier { } } } + + @ViewBuilder + private func backgroundView(for background: BackgroundValue) -> some View { + switch background { + case let .color(color): + color + case let .linearGradient(gradient, start, end): + LinearGradient(gradient: gradient, startPoint: start, endPoint: end) + case let .radialGradient(spec): + radialGradientBackground(spec) + case let .angularGradient(gradient, center, angle): + AngularGradient(gradient: gradient, center: center, angle: angle) + } + } } diff --git a/packages/ios-client/ios/ui/Style/StyleConverter.swift b/packages/ios-client/ios/ui/Style/StyleConverter.swift index cabacce6..e7698f30 100644 --- a/packages/ios-client/ios/ui/Style/StyleConverter.swift +++ b/packages/ios-client/ios/ui/Style/StyleConverter.swift @@ -135,6 +135,7 @@ enum StyleConverter { return DecorationStyle( backgroundColor: JSStyleParser.background(js["backgroundColor"]), + backgroundImage: JSStyleParser.background(js["backgroundImage"]), cornerRadius: JSStyleParser.number(js["borderRadius"]), border: border, shadow: shadow, diff --git a/packages/ios-client/ios/ui/Views/VoltraChart.swift b/packages/ios-client/ios/ui/Views/VoltraChart.swift index 95529d3b..6359f205 100644 --- a/packages/ios-client/ios/ui/Views/VoltraChart.swift +++ b/packages/ios-client/ios/ui/Views/VoltraChart.swift @@ -658,6 +658,7 @@ private extension View { let decorationForLayout: DecorationStyle = hasClipping ? DecorationStyle( backgroundColor: decoration.backgroundColor, + backgroundImage: decoration.backgroundImage, cornerRadius: nil, border: decoration.border, shadow: decoration.shadow, diff --git a/packages/ios-client/ios/ui/Views/VoltraLinearGradient.swift b/packages/ios-client/ios/ui/Views/VoltraLinearGradient.swift index 53edcdd4..458c8498 100644 --- a/packages/ios-client/ios/ui/Views/VoltraLinearGradient.swift +++ b/packages/ios-client/ios/ui/Views/VoltraLinearGradient.swift @@ -91,7 +91,7 @@ public struct VoltraLinearGradient: VoltraView { let (layout, baseDecoration, rendering, text) = StyleConverter.convert(anyStyle) var decoration = baseDecoration - decoration.backgroundColor = .linearGradient(gradient: gradient, startPoint: start, endPoint: end) + decoration.backgroundImage = .linearGradient(gradient: gradient, startPoint: start, endPoint: end) if let widget = voltraEnvironment.widget, widget.isHomeScreenWidget, diff --git a/packages/ios/src/styles/types.ts b/packages/ios/src/styles/types.ts index 2e0257b6..2cf8c4aa 100644 --- a/packages/ios/src/styles/types.ts +++ b/packages/ios/src/styles/types.ts @@ -44,6 +44,7 @@ export type VoltraViewStyle = { marginHorizontal?: number | string marginVertical?: number | string backgroundColor?: string + backgroundImage?: string opacity?: number borderRadius?: number | string borderWidth?: number diff --git a/website/docs/ios/development/styling.md b/website/docs/ios/development/styling.md index 473a3a26..2bd3aa0f 100644 --- a/website/docs/ios/development/styling.md +++ b/website/docs/ios/development/styling.md @@ -32,6 +32,7 @@ The following React Native style properties are supported: **Style:** - `backgroundColor` - Background color (hex strings, color names, or CSS gradient strings — see [Gradients](#gradients)) +- `backgroundImage` - Gradient layer rendered above `backgroundColor` when both are present - `opacity` - Opacity value between 0 and 1 - `borderRadius` - Corner radius value - `borderWidth` - Border width @@ -135,7 +136,9 @@ const element = ( ## Gradients -The `backgroundColor` style property accepts CSS gradient strings in addition to solid colors. Gradients are rendered natively using SwiftUI gradient modifiers and are automatically clipped by `borderRadius`. +The `backgroundImage` style property is the preferred way to render CSS gradients. It is painted above `backgroundColor`, so you can combine a solid or semi-transparent base color with a gradient overlay. + +For backward compatibility, `backgroundColor` still accepts CSS gradient strings in addition to solid colors. Gradients are rendered natively using SwiftUI gradient modifiers and are automatically clipped by `borderRadius`. Invalid or unsupported gradient syntax is parsed in **strict mode** and results in **no gradient background** (instead of silent best-effort fallback). @@ -231,7 +234,7 @@ Passing a plain color string to `backgroundColor` continues to work exactly as b ### Comparison with `` component -| Feature | `backgroundColor` gradient | `` component | +| Feature | `backgroundImage` / `backgroundColor` gradient | `` component | |---|---|---| | CSS string syntax | ✓ | — | | Named directions (`to right`) | ✓ (physical direction) | ✓ | @@ -245,7 +248,7 @@ Passing a plain color string to `backgroundColor` continues to work exactly as b | Dithering | — | ✓ | | Children layered on top | ✓ (as background) | ✓ (as container) | -Use `backgroundColor` gradient strings for convenience and web-style syntax. Use `` when you need precise `{x, y}` coordinate control or dithering. +Use `backgroundImage` for web-style gradient layering. Keep `backgroundColor` gradients if you need backward compatibility, and use `` when you need precise `{x, y}` coordinate control or dithering. ### Scope and exclusions From a0fd2e478d2122ee56f3219a0ace116e543f0b21 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 11 Jun 2026 14:23:23 +0200 Subject: [PATCH 2/5] docs(ios): prefer backgroundImage gradient examples Update iOS styling docs to show backgroundImage as the primary gradient prop while keeping backgroundColor gradient usage documented for compatibility. --- website/docs/ios/development/styling.md | 33 +++++++++++++++---------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/website/docs/ios/development/styling.md b/website/docs/ios/development/styling.md index 2bd3aa0f..3dc79e81 100644 --- a/website/docs/ios/development/styling.md +++ b/website/docs/ios/development/styling.md @@ -136,7 +136,7 @@ const element = ( ## Gradients -The `backgroundImage` style property is the preferred way to render CSS gradients. It is painted above `backgroundColor`, so you can combine a solid or semi-transparent base color with a gradient overlay. +The `backgroundImage` style property is the preferred way to render CSS gradients. It is painted above `backgroundColor`, so you can combine a solid base color with a semi-transparent gradient overlay. For backward compatibility, `backgroundColor` still accepts CSS gradient strings in addition to solid colors. Gradients are rendered natively using SwiftUI gradient modifiers and are automatically clipped by `borderRadius`. @@ -146,14 +146,14 @@ Invalid or unsupported gradient syntax is parsed in **strict mode** and results ```tsx // Named direction - + // Diagonal - + // Angle in degrees/radians/turns - - + + ``` Supported directions: `to right`, `to left`, `to top`, `to bottom`, `to top right`, `to top left`, `to bottom right`, `to bottom left`. @@ -165,7 +165,7 @@ Explicit percentage positions are supported: ```tsx ``` @@ -180,7 +180,8 @@ When positions are omitted, Voltra applies CSS-like stop fix-up: ```tsx ``` @@ -188,9 +189,9 @@ When positions are omitted, Voltra applies CSS-like stop fix-up: ### Radial gradients ```tsx - - - + + + ``` :::note @@ -202,8 +203,8 @@ For `ellipse`, SwiftUI does not provide native elliptical radial gradients. Volt ### Conic gradients ```tsx - - + + ``` ### With border radius @@ -213,7 +214,7 @@ Gradients are clipped by `borderRadius` automatically — no extra configuration ```tsx ``` +For backward compatibility, `backgroundColor` can still receive gradient strings: + +```tsx + +``` + ### Comparison with `` component | Feature | `backgroundImage` / `backgroundColor` gradient | `` component | From f39cc6ea5a5221cbcdf7fcc128db2ba79ed8e79c Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 11 Jun 2026 16:18:20 +0200 Subject: [PATCH 3/5] docs(ios): remove backgroundColor gradient example Keep the compatibility note but avoid showing backgroundColor gradient usage now that backgroundImage is the preferred API. --- website/docs/ios/development/styling.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/website/docs/ios/development/styling.md b/website/docs/ios/development/styling.md index 3dc79e81..d90f1ad0 100644 --- a/website/docs/ios/development/styling.md +++ b/website/docs/ios/development/styling.md @@ -233,11 +233,7 @@ Passing a plain color string to `backgroundColor` continues to work exactly as b ``` -For backward compatibility, `backgroundColor` can still receive gradient strings: - -```tsx - -``` +For backward compatibility, `backgroundColor` can still receive gradient strings, but new code should use `backgroundImage`. ### Comparison with `` component From f9063b005027975e7ed26785d6f3e66d2ac6de8e Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 12 Jun 2026 14:46:36 +0200 Subject: [PATCH 4/5] fix(ios): use ordered background modifiers Replace the internal ZStack background layering with ordered SwiftUI background modifiers while preserving backgroundColor behind backgroundImage. --- .../ios/ui/Style/DecorationStyle.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/ios-client/ios/ui/Style/DecorationStyle.swift b/packages/ios-client/ios/ui/Style/DecorationStyle.swift index f4b9f41c..6c14ada7 100644 --- a/packages/ios-client/ios/ui/Style/DecorationStyle.swift +++ b/packages/ios-client/ios/ui/Style/DecorationStyle.swift @@ -152,14 +152,14 @@ struct DecorationModifier: ViewModifier { func body(content: Content) -> some View { content - .background { - ZStack { - if let bg = resolvedBackgroundColor { - backgroundView(for: bg) - } - if let bg = resolvedBackgroundImage { - backgroundView(for: bg) - } + .voltraIfLet(resolvedBackgroundImage) { content, bg in + content.background { + backgroundView(for: bg) + } + } + .voltraIfLet(resolvedBackgroundColor) { content, bg in + content.background { + backgroundView(for: bg) } } // If we have a corner radius, we must handle the border specifically here From f8824660b0a97efe1579f3e1990d5bd34f487b0f Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 12 Jun 2026 14:57:12 +0200 Subject: [PATCH 5/5] test(ios): add composed background example Update the iOS gradient playground to exercise backgroundImage and add a composed background section combining backgroundColor with a translucent gradient layer. --- example/screens/ios/tabs/sections.ts | 2 +- .../GradientPlaygroundScreen.tsx | 38 +++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/example/screens/ios/tabs/sections.ts b/example/screens/ios/tabs/sections.ts index 9b3b8d1d..ad3506b6 100644 --- a/example/screens/ios/tabs/sections.ts +++ b/example/screens/ios/tabs/sections.ts @@ -92,7 +92,7 @@ export const IOS_OTHER_SECTIONS: ExampleSection[] = [ id: 'gradient-playground', title: 'Gradient Playground', description: - 'Test CSS gradient strings as backgroundColor. Experiment with linear, radial, and conic gradients, direction and angle controls, color presets, stop positions, and borderRadius clipping.', + 'Test CSS gradient strings as backgroundImage. Experiment with linear, radial, and conic gradients, composed backgrounds, stop positions, and borderRadius clipping.', route: '/testing-grounds/gradient-playground', }, { diff --git a/example/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen.tsx b/example/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen.tsx index 35bd5b1f..e7c031e8 100644 --- a/example/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen.tsx +++ b/example/screens/testing-grounds/gradient-playground/GradientPlaygroundScreen.tsx @@ -85,7 +85,7 @@ export default function GradientPlaygroundScreen() { return ( Playground uses parser-compatible CSS syntax only. If a preview is blank, this indicates a gradient parser bug @@ -159,7 +159,7 @@ export default function GradientPlaygroundScreen() { + {/* backgroundColor + backgroundImage composition */} + + Composed Background + + backgroundColor: "#101828" with backgroundImage: "linear-gradient(... transparent ...)" + + + + + + Solid base + image layer + + + Transparent pixels reveal the backgroundColor + + + + + {/* Solid color still works */} Solid Color (Unchanged)