249 lines
9.3 KiB
Markdown
249 lines
9.3 KiB
Markdown
---
|
|
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.
|