Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion example/screens/ios/tabs/sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default function GradientPlaygroundScreen() {
return (
<ScreenLayout
title="Gradient Playground"
description="Test CSS gradient strings as backgroundColor on Voltra views."
description="Test CSS gradient strings as backgroundImage on Voltra views."
>
<Text style={styles.warningText}>
Playground uses parser-compatible CSS syntax only. If a preview is blank, this indicates a gradient parser bug
Expand Down Expand Up @@ -159,7 +159,7 @@ export default function GradientPlaygroundScreen() {
<Voltra.View
style={{
height: '100%',
backgroundColor: gradient,
backgroundImage: gradient,
borderRadius,
width: '100%',
alignItems: 'center',
Expand All @@ -181,7 +181,7 @@ export default function GradientPlaygroundScreen() {
<Voltra.View
style={{
height: '100%',
backgroundColor: 'linear-gradient(to right, red 10%, yellow 50%, blue 90%)',
backgroundImage: 'linear-gradient(to right, red 10%, yellow 50%, blue 90%)',
borderRadius: 8,
width: '100%',
}}
Expand All @@ -200,14 +200,44 @@ export default function GradientPlaygroundScreen() {
<Voltra.View
style={{
height: '100%',
backgroundColor: 'linear-gradient(to right, rgba(255,0,0,0.8) 0%, rgba(0,0,255,0.3) 100%)',
backgroundImage: 'linear-gradient(to right, rgba(255,0,0,0.8) 0%, rgba(0,0,255,0.3) 100%)',
borderRadius: 8,
width: '100%',
}}
/>
</VoltraView>
</Card>

{/* backgroundColor + backgroundImage composition */}
<Card>
<Card.Title>Composed Background</Card.Title>
<Text style={styles.previewSubtext}>
backgroundColor: "#101828" with backgroundImage: "linear-gradient(... transparent ...)"
</Text>

<VoltraView style={{ width: '100%', height: 120, backgroundColor: '#0F172A', padding: 16, marginTop: 12 }}>
<Voltra.View
style={{
height: '100%',
backgroundColor: '#101828',
backgroundImage:
'linear-gradient(to bottom right, rgba(34,211,238,0.86) 0%, rgba(236,72,153,0.42) 52%, transparent 100%)',
borderRadius: 14,
width: '100%',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Voltra.Text style={{ color: '#FFFFFF', fontSize: 14, fontWeight: '700' }}>
Solid base + image layer
</Voltra.Text>
<Voltra.Text style={{ color: 'rgba(255,255,255,0.72)', fontSize: 11 }}>
Transparent pixels reveal the backgroundColor
</Voltra.Text>
</Voltra.View>
</VoltraView>
</Card>

{/* Solid color still works */}
<Card>
<Card.Title>Solid Color (Unchanged)</Card.Title>
Expand Down
41 changes: 30 additions & 11 deletions packages/ios-client/ios/ui/Style/DecorationStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)?
Expand Down Expand Up @@ -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
Expand All @@ -143,18 +152,14 @@ struct DecorationModifier: ViewModifier {

func body(content: Content) -> some View {
content
.voltraIfLet(resolvedBackgroundImage) { content, bg in
content.background {
backgroundView(for: bg)
}
}
.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)
}
case let .angularGradient(gradient, center, angle):
content.background(AngularGradient(gradient: gradient, center: center, angle: angle))
content.background {
backgroundView(for: bg)
}
}
// If we have a corner radius, we must handle the border specifically here
Expand Down Expand Up @@ -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)
}
}
}
1 change: 1 addition & 0 deletions packages/ios-client/ios/ui/Style/StyleConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/ios-client/ios/ui/Views/VoltraChart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/ios/src/styles/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type VoltraViewStyle = {
marginHorizontal?: number | string
marginVertical?: number | string
backgroundColor?: string
backgroundImage?: string
opacity?: number
borderRadius?: number | string
borderWidth?: number
Expand Down
36 changes: 21 additions & 15 deletions website/docs/ios/development/styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -135,22 +136,24 @@ 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 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`.

Invalid or unsupported gradient syntax is parsed in **strict mode** and results in **no gradient background** (instead of silent best-effort fallback).

### Linear gradients

```tsx
// Named direction
<Voltra.View style={{ backgroundColor: 'linear-gradient(to right, #6366F1, #EC4899)' }} />
<Voltra.View style={{ backgroundImage: 'linear-gradient(to right, #6366F1, #EC4899)' }} />

// Diagonal
<Voltra.View style={{ backgroundColor: 'linear-gradient(to bottom right, #0EA5E9, #8B5CF6)' }} />
<Voltra.View style={{ backgroundImage: 'linear-gradient(to bottom right, #0EA5E9, #8B5CF6)' }} />

// Angle in degrees/radians/turns
<Voltra.View style={{ backgroundColor: 'linear-gradient(45deg, #FF6B6B, #FFD93D)' }} />
<Voltra.View style={{ backgroundColor: 'linear-gradient(0.25turn, #FF6B6B, #FFD93D)' }} />
<Voltra.View style={{ backgroundImage: 'linear-gradient(45deg, #FF6B6B, #FFD93D)' }} />
<Voltra.View style={{ backgroundImage: 'linear-gradient(0.25turn, #FF6B6B, #FFD93D)' }} />
```

Supported directions: `to right`, `to left`, `to top`, `to bottom`, `to top right`, `to top left`, `to bottom right`, `to bottom left`.
Expand All @@ -162,7 +165,7 @@ Explicit percentage positions are supported:
```tsx
<Voltra.View
style={{
backgroundColor: 'linear-gradient(to right, red 0%, yellow 50%, blue 100%)',
backgroundImage: 'linear-gradient(to right, red 0%, yellow 50%, blue 100%)',
}}
/>
```
Expand All @@ -177,17 +180,18 @@ When positions are omitted, Voltra applies CSS-like stop fix-up:
```tsx
<Voltra.View
style={{
backgroundColor: 'linear-gradient(to right, rgba(255,0,0,0.8), rgba(0,0,255,0.3))',
backgroundColor: '#101828',
backgroundImage: 'linear-gradient(to right, rgba(255,0,0,0.8), rgba(0,0,255,0.3))',
}}
/>
```

### Radial gradients

```tsx
<Voltra.View style={{ backgroundColor: 'radial-gradient(#FF6B6B, #6366F1)' }} />
<Voltra.View style={{ backgroundColor: 'radial-gradient(circle at top right, #FF6B6B, #6366F1)' }} />
<Voltra.View style={{ backgroundColor: 'radial-gradient(closest-side at left bottom, #FF6B6B, #6366F1)' }} />
<Voltra.View style={{ backgroundImage: 'radial-gradient(#FF6B6B, #6366F1)' }} />
<Voltra.View style={{ backgroundImage: 'radial-gradient(circle at top right, #FF6B6B, #6366F1)' }} />
<Voltra.View style={{ backgroundImage: 'radial-gradient(closest-side at left bottom, #FF6B6B, #6366F1)' }} />
```

:::note
Expand All @@ -199,8 +203,8 @@ For `ellipse`, SwiftUI does not provide native elliptical radial gradients. Volt
### Conic gradients

```tsx
<Voltra.View style={{ backgroundColor: 'conic-gradient(from 45deg, red, blue)' }} />
<Voltra.View style={{ backgroundColor: 'conic-gradient(from 45deg at top right, red 0%, blue 75%)' }} />
<Voltra.View style={{ backgroundImage: 'conic-gradient(from 45deg, red, blue)' }} />
<Voltra.View style={{ backgroundImage: 'conic-gradient(from 45deg at top right, red 0%, blue 75%)' }} />
```

### With border radius
Expand All @@ -210,7 +214,7 @@ Gradients are clipped by `borderRadius` automatically — no extra configuration
```tsx
<Voltra.View
style={{
backgroundColor: 'linear-gradient(to right, #6366F1, #EC4899)',
backgroundImage: 'linear-gradient(to right, #6366F1, #EC4899)',
borderRadius: 16,
padding: 16,
}}
Expand All @@ -229,9 +233,11 @@ Passing a plain color string to `backgroundColor` continues to work exactly as b
<Voltra.View style={{ backgroundColor: 'rgba(0,0,0,0.5)' }} />
```

For backward compatibility, `backgroundColor` can still receive gradient strings, but new code should use `backgroundImage`.

### Comparison with `<LinearGradient>` component

| Feature | `backgroundColor` gradient | `<LinearGradient>` component |
| Feature | `backgroundImage` / `backgroundColor` gradient | `<LinearGradient>` component |
|---|---|---|
| CSS string syntax | ✓ | — |
| Named directions (`to right`) | ✓ (physical direction) | ✓ |
Expand All @@ -245,7 +251,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 `<LinearGradient>` 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 `<LinearGradient>` when you need precise `{x, y}` coordinate control or dithering.

### Scope and exclusions

Expand Down