📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
---
|
||||
name: neumorphism
|
||||
description: Web and App implementation guide for Neumorphism (Soft UI). Trigger when user wants soft shadows, extruded appearance, and light source simulation.
|
||||
date_added: "2026-06-17"
|
||||
risk: safe
|
||||
source: self
|
||||
source_type: self
|
||||
---
|
||||
|
||||
# Neumorphism (Soft UI)
|
||||
|
||||
> "Elements extruded from the background material itself, shaped by a singular, persistent light source."
|
||||
|
||||
|
||||
## When to Use
|
||||
Use this sub-style when the user's request matches the aesthetic described above. This is a child reference of the `design-it` skill and is not meant to be triggered directly.
|
||||
|
||||
## Core Principles
|
||||
1. **Unified Surface Color**: The background and the elements MUST share the exact same base color.
|
||||
2. **Dual Shadows**: Elements are shaped by two shadows: a light shadow (highlight) on the side facing the light source, and a dark shadow on the opposite side.
|
||||
3. **No Borders**: The shape is entirely defined by the shadows.
|
||||
|
||||
## Visual DNA
|
||||
- **Colors**: Works best with mid-tone neutrals. **Desert Mirage**, **Earth-Grounded Elegance**, or **Sophisticated Neutral** are perfect. Avoid pure white or pure black (shadows/highlights won't show).
|
||||
- **Typography**: Soft, rounded sans-serifs (e.g., `Nunito`, `Quicksand`).
|
||||
- **Shapes**: Pill shapes, rounded rectangles. Sharp corners break the illusion of extruded material.
|
||||
|
||||
## Web Implementation
|
||||
- The magic is entirely in `box-shadow` manipulating light and dark variants of the base color.
|
||||
- **CSS Example**:
|
||||
```css
|
||||
:root {
|
||||
--base-color: #E6E2DD; /* From Sophisticated Neutral */
|
||||
--highlight: #ffffff;
|
||||
--shadow: #c4c0bc;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--base-color);
|
||||
}
|
||||
|
||||
.neu-element {
|
||||
background-color: var(--base-color);
|
||||
border-radius: 20px;
|
||||
/* Top-left highlight, Bottom-right shadow */
|
||||
box-shadow: 9px 9px 18px var(--shadow),
|
||||
-9px -9px 18px var(--highlight);
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.neu-pressed {
|
||||
/* Inset shadows for pressed/active state */
|
||||
border-radius: 20px;
|
||||
background: var(--base-color);
|
||||
box-shadow: inset 9px 9px 18px var(--shadow),
|
||||
inset -9px -9px 18px var(--highlight);
|
||||
}
|
||||
```
|
||||
|
||||
## App Implementation
|
||||
|
||||
### SwiftUI
|
||||
```swift
|
||||
struct NeuCard: View {
|
||||
let baseColor = Color(red: 0.90, green: 0.89, blue: 0.87) // #E6E2DD
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 24) {
|
||||
Text("Neumorphic Card")
|
||||
.font(.system(size: 20, weight: .semibold, design: .rounded))
|
||||
|
||||
Text("Extruded from the surface itself.")
|
||||
.font(.system(size: 15, design: .rounded))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(32)
|
||||
.background(baseColor)
|
||||
.cornerRadius(20)
|
||||
// Light shadow (top-left)
|
||||
.shadow(color: Color.white.opacity(0.7), radius: 10, x: -8, y: -8)
|
||||
// Dark shadow (bottom-right)
|
||||
.shadow(color: Color.black.opacity(0.15), radius: 10, x: 8, y: 8)
|
||||
}
|
||||
}
|
||||
|
||||
// Pressed / inset neumorphic button
|
||||
struct NeuButton: View {
|
||||
@State private var isPressed = false
|
||||
let baseColor = Color(red: 0.90, green: 0.89, blue: 0.87)
|
||||
|
||||
var body: some View {
|
||||
Button(action: {}) {
|
||||
Text("Press Me")
|
||||
.font(.system(size: 16, weight: .semibold, design: .rounded))
|
||||
.foregroundColor(.primary)
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
.background(
|
||||
Group {
|
||||
if isPressed {
|
||||
// Inset effect using inner shadow (ZStack trick)
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(baseColor)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.stroke(baseColor, lineWidth: 4)
|
||||
.shadow(color: Color.black.opacity(0.2), radius: 4, x: 4, y: 4)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.stroke(baseColor, lineWidth: 4)
|
||||
.shadow(color: Color.white.opacity(0.7), radius: 4, x: -4, y: -4)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
)
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(baseColor)
|
||||
.shadow(color: Color.white.opacity(0.7), radius: 10, x: -8, y: -8)
|
||||
.shadow(color: Color.black.opacity(0.15), radius: 10, x: 8, y: 8)
|
||||
}
|
||||
}
|
||||
)
|
||||
.buttonStyle(.plain)
|
||||
.simultaneousGesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { _ in isPressed = true }
|
||||
.onEnded { _ in isPressed = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
- The key trick: two `.shadow()` modifiers — one white (top-left), one dark (bottom-right).
|
||||
- Inner shadow (pressed state) requires a ZStack/overlay hack since SwiftUI doesn't have native `inset` shadows. Clip stroked shapes to simulate.
|
||||
- The view's background color MUST match its parent's background exactly.
|
||||
|
||||
### Flutter
|
||||
```dart
|
||||
class NeuCard extends StatelessWidget {
|
||||
final Color baseColor = const Color(0xFFE6E2DD);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
// Dark shadow (bottom-right)
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
offset: const Offset(8, 8),
|
||||
blurRadius: 16,
|
||||
),
|
||||
// Light shadow (top-left)
|
||||
BoxShadow(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
offset: const Offset(-8, -8),
|
||||
blurRadius: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text('Neumorphic Card',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 16),
|
||||
Text('Extruded from the surface itself.',
|
||||
style: TextStyle(fontSize: 15, color: Colors.black54)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Inner shadows** (for pressed state) are NOT natively supported in Flutter's `BoxShadow`.
|
||||
- Use the `flutter_inset_box_shadow` package OR fake it with a layered `Stack`: place a `Container` with a dark gradient overlay on top and a light gradient below.
|
||||
- Set the `Scaffold` background to the SAME `baseColor` so elements look extruded.
|
||||
|
||||
### React Native
|
||||
```jsx
|
||||
const NeuCard = () => (
|
||||
<View style={{
|
||||
padding: 32,
|
||||
backgroundColor: '#E6E2DD',
|
||||
borderRadius: 20,
|
||||
// Light shadow (top-left) — iOS only supports one shadow
|
||||
shadowColor: '#FFFFFF',
|
||||
shadowOffset: { width: -8, height: -8 },
|
||||
shadowOpacity: 0.7,
|
||||
shadowRadius: 10,
|
||||
// Android — use elevation for basic shadow
|
||||
elevation: 8,
|
||||
}}>
|
||||
<Text style={{ fontSize: 20, fontWeight: '600' }}>Neumorphic Card</Text>
|
||||
<Text style={{ fontSize: 15, color: '#888', marginTop: 16 }}>
|
||||
Extruded from the surface itself.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
```
|
||||
- **Major limitation**: React Native only supports ONE shadow per view. True neumorphism requires TWO opposing shadows.
|
||||
- **Workaround**: Use `react-native-shadow-2` or `react-native-neomorph-shadows` which provide multi-shadow support.
|
||||
- Alternative: Wrap two nested `View`s — the outer one has the dark shadow, the inner one has the light shadow.
|
||||
- Inner shadows for pressed states require SVG-based solutions or pre-rendered images.
|
||||
|
||||
### Jetpack Compose
|
||||
```kotlin
|
||||
@Composable
|
||||
fun NeuCard() {
|
||||
val baseColor = Color(0xFFE6E2DD)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(24.dp)
|
||||
.shadow(
|
||||
elevation = 8.dp,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
ambientColor = Color.Black.copy(alpha = 0.15f),
|
||||
spotColor = Color.Black.copy(alpha = 0.15f),
|
||||
)
|
||||
.background(baseColor, RoundedCornerShape(20.dp))
|
||||
.padding(32.dp)
|
||||
) {
|
||||
Column {
|
||||
Text("Neumorphic Card",
|
||||
fontSize = 20.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text("Extruded from the surface itself.",
|
||||
fontSize = 15.sp, color = Color(0xFF888888))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Compose limitation**: Like React Native, Compose `Modifier.shadow()` only supports a single directional shadow.
|
||||
- For true dual-shadow neumorphism, use a custom `Modifier.drawBehind { }` with `drawIntoCanvas` to paint two separate shadow paths (one light, one dark).
|
||||
- The `neumorphic-compose` library provides a pre-built `Modifier.neumorphic()` that handles both shadows.
|
||||
- Set the `Scaffold` background to the same `baseColor` — this is non-negotiable.
|
||||
|
||||
## Do's and Don'ts
|
||||
- **DO**: Use inset shadows for "active" states (like pressed buttons or filled form fields).
|
||||
- **DON'T**: Rely on Neumorphism for critical elements without secondary indicators. Contrast is inherently low, making it an accessibility nightmare if used improperly.
|
||||
|
||||
## Limitations
|
||||
- This is a styling reference and does not replace environment-specific validation, accessibility testing, or expert review.
|
||||
- Ensure appropriate contrast ratios and responsive behaviors are verified separately.
|
||||
Reference in New Issue
Block a user