Design Metro-style tile interfaces with grid layouts
Metro/Tile Design sub-style for design-it: sharp square tiles, live-updating content, and horizontal-flow layout tricks per platform.
Maintainer of this project? Claim this page to edit the listing.
13.6.1Add to Favorites
Why it matters
Create modern Metro UI (Windows 8/10-style) tile-based interfaces with clean grid layouts, flat design principles, and responsive tile components for web and mobile applications.
Outcomes
What it gets done
Generate HTML/CSS for flat, grid-based tile layouts with proper spacing
Design responsive tile components that adapt to different screen sizes
Implement Metro design language with typography, colors, and iconography
Create interactive tile states including hover, active, and live tile animations
Install
Add it to your toolbox
Run in your project directory:
curl -fsSL https://spark.entire.vc/get/ag-tile-design | bash Overview
Tile Design (Metro UI)
A design-it sub-style for Metro/Windows-Phone tile UI - sharp-cornered flat tiles, live-updating content, and horizontal-flow grid layouts per platform. Use for a Metro-style tile dashboard or launcher needing flat colors, live tiles, and horizontal panning; never add drop shadows or gradients.
What it does
Tile Design (Metro UI) is a design-it sub-style built on flat, colored squares with zero border-radius, live-updating internal content (tiles flip, scroll, or fade to reveal new data without user interaction), and a grid that expands infinitely to the right, encouraging horizontal panning - the Windows Phone/Windows 8 start-screen look. It is a child reference of design-it, applied when a request matches this specific aesthetic rather than triggered directly.
When to use - and when NOT to
Use this sub-style for dashboards or launchers that want the Metro/Windows Phone tile grid - flat saturated colors on a near-black background, wireframe monochrome icons, and Segoe UI-style typography. Its own rules are strict: never add drop shadows or gradients to tiles, and always place the label text in the tile's bottom-left corner - deviating from either breaks the flat, purely-typographic Metro look this style depends on.
Inputs and outputs
On web, tiles sit in a CSS grid (repeat(4, 150px) columns, 150px auto-rows) with tile-wide/tile-large classes spanning 2 columns or a full 2x2 block, a :active { transform: scale(0.95) } "tilt" click effect, and live-tile content animated via a slideUp keyframe that cycles translateY to simulate content flipping. Native implementations diverge in how they achieve the horizontal-flowing grid: SwiftUI uses a LazyHGrid inside a horizontal ScrollView (the most direct match), Flutter and React Native both achieve it with a column-first Wrap/flexWrap trick - a fixed-height container with direction: Axis.vertical (Flutter) or flexDirection: 'column' (React Native) inside a horizontal scroll, forcing children into columns that then flow left to right. Jetpack Compose's LazyHorizontalGrid is the closest built-in match but the skill notes it doesn't easily support tiles spanning multiple rows for a true 2x2 "large" tile - achieving that requires a custom Layout or a staggered grid.
.tile:active { transform: scale(0.95); }
.tile-large { grid-column: span 2; grid-row: span 2; }
Integrations
As a design-it sub-style, it's invoked through the parent skill's aesthetic matching, and the tilt-press interaction is reproduced with each platform's own tools: SwiftUI's isPressed state driving .scaleEffect and a spring animation, Flutter's GestureDetector plus AnimatedScale, React Native's Animated.View with TouchableWithoutFeedback, and Compose's Modifier.scale() paired with detectTapGestures.
Who it's for
Designers and developers building a Metro/Windows-Phone-style tile dashboard or launcher who need the flat-color, live-tile, horizontally-panning grid reproduced consistently across web and native platforms - including the specific column-first flex-wrap workaround needed on Flutter and React Native to get tiles flowing left-to-right instead of top-to-bottom.
Source README
Tile Design (Metro UI)
"Authentically digital. Clean, sharp squares relying purely on typography and flat color."
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
- Sharp Corners: Absolutely no border-radius. Everything is a perfect square or sharp rectangle.
- Live Data: Tiles flip, scroll, or fade internally to show live updates without the user interacting.
- Horizontal Panning: The grid often expands infinitely to the right, encouraging horizontal scrolling.
Visual DNA
- Colors: High saturation, flat colors. A dark background (pure black) with bright cyan, magenta, orange, and green tiles.
- Typography: Extremely clean, light sans-serifs (like
Segoe UI Light). Text is almost always pure white. - Icons: Simple, wireframe, monochromatic glyphs placed centrally or in the corner.
Web Implementation
- CSS Example:
body {
background-color: #111;
color: #fff;
font-family: 'Segoe UI', sans-serif;
overflow-x: auto; /* Horizontal scroll */
}
.tile-group {
display: grid;
grid-template-columns: repeat(4, 150px);
grid-auto-rows: 150px;
gap: 8px;
padding: 40px;
}
.tile {
background-color: #0078D7; /* Classic Windows Blue */
padding: 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
cursor: pointer;
/* The "tilt" click effect */
transition: transform 0.1s;
transform-origin: center;
}
.tile:active {
transform: scale(0.95);
}
.tile-wide { grid-column: span 2; }
.tile-large { grid-column: span 2; grid-row: span 2; }
/* Live Tile Animation */
.tile-live-content {
animation: slideUp 5s infinite;
}
@keyframes slideUp {
0%, 45% { transform: translateY(0); }
50%, 95% { transform: translateY(-100%); } /* Slides up to reveal next item */
100% { transform: translateY(0); }
}
App Implementation
SwiftUI
struct TileDesignView: View {
let rows = [GridItem(.fixed(150), spacing: 8), GridItem(.fixed(150), spacing: 8)]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: rows, spacing: 8) {
TileView(title: "Mail", color: Color(hex: "0078D7"), icon: "envelope")
TileView(title: "Photos", color: Color(hex: "00CC6A"), icon: "photo", isLarge: true)
TileView(title: "Weather", color: Color(hex: "2D7D9A"), icon: "cloud.sun")
TileView(title: "Calendar", color: Color(hex: "D13438"), icon: "calendar")
}
.padding(40)
}
.background(Color(hex: "111111").ignoresSafeArea())
}
}
struct TileView: View {
let title: String
let color: Color
let icon: String
var isLarge: Bool = false
@State private var isPressed = false
var body: some View {
VStack(alignment: .leading) {
Image(systemName: icon)
.font(.system(size: 32, weight: .light))
.foregroundColor(.white)
Spacer()
Text(title)
.font(.custom("Segoe UI", size: 16))
.foregroundColor(.white)
}
.padding(16)
// Sharp corners are mandatory
.frame(width: isLarge ? 308 : 150, height: isLarge ? 308 : 150, alignment: .leading)
.background(color)
.scaleEffect(isPressed ? 0.95 : 1.0)
.animation(.spring(response: 0.2, dampingFraction: 0.5), value: isPressed)
.onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: { pressing in
isPressed = pressing
}, perform: {})
}
}
- A
LazyHGridinside a horizontalScrollViewperfectly replicates the Windows Phone / Windows 8 start screen. - Absolutely NO corner radius.
- The
isPressedstate triggering a.scaleEffect(0.95)replicates the physical "tilt" interaction of Metro tiles.
Flutter
class TileDesignScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF111111),
body: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.all(40),
child: SizedBox(
height: 308, // Two rows of 150px + 8px spacing
child: Wrap(
direction: Axis.vertical,
spacing: 8,
runSpacing: 8,
children: [
_buildTile('Mail', const Color(0xFF0078D7), Icons.mail_outline),
_buildTile('Weather', const Color(0xFF2D7D9A), Icons.cloud_outlined),
_buildTile('Photos', const Color(0xFF00CC6A), Icons.photo_outlined, isLarge: true),
_buildTile('Calendar', const Color(0xFFD13438), Icons.calendar_today),
],
),
),
),
);
}
Widget _buildTile(String title, Color color, IconData icon, {bool isLarge = false}) {
return StatefulBuilder(
builder: (context, setState) {
bool isPressed = false;
return GestureDetector(
onTapDown: (_) => setState(() => isPressed = true),
onTapUp: (_) => setState(() => isPressed = false),
onTapCancel: () => setState(() => isPressed = false),
child: AnimatedScale(
scale: isPressed ? 0.95 : 1.0,
duration: const Duration(milliseconds: 100),
child: Container(
width: isLarge ? 308 : 150,
height: isLarge ? 308 : 150,
color: color, // Sharp corners
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(icon, color: Colors.white, size: 32),
Text(title, style: const TextStyle(color: Colors.white, fontFamily: 'Segoe UI', fontSize: 16)),
],
),
),
),
);
}
);
}
}
Wrapwithdirection: Axis.verticalinside a horizontally scrollingSizedBoxis the easiest way to build a Metro grid that flows left-to-right.- Wrap tiles in
GestureDetectorandAnimatedScaleto handle the press animation.
React Native
const TileDesignScreen = () => {
return (
<ScrollView horizontal style={{ flex: 1, backgroundColor: '#111' }} contentContainerStyle={{ padding: 40 }}>
<View style={{ flexDirection: 'column', flexWrap: 'wrap', height: 308, gap: 8 }}>
<Tile title="Mail" color="#0078D7" />
<Tile title="Weather" color="#2D7D9A" />
<Tile title="Photos" color="#00CC6A" isLarge />
<Tile title="Calendar" color="#D13438" />
</View>
</ScrollView>
);
};
const Tile = ({ title, color, isLarge }) => {
const scale = useRef(new Animated.Value(1)).current;
const handlePressIn = () => Animated.spring(scale, { toValue: 0.95, useNativeDriver: true }).start();
const handlePressOut = () => Animated.spring(scale, { toValue: 1, useNativeDriver: true }).start();
return (
<TouchableWithoutFeedback onPressIn={handlePressIn} onPressOut={handlePressOut}>
<Animated.View style={{
width: isLarge ? 308 : 150, height: isLarge ? 308 : 150,
backgroundColor: color, padding: 16, justifyContent: 'space-between',
transform: [{ scale }] // The Metro tilt effect
}}>
<View style={{ width: 32, height: 32, backgroundColor: '#FFF', opacity: 0.5 }} />
<Text style={{ color: '#FFF', fontFamily: 'Segoe UI', fontSize: 16 }}>{title}</Text>
</Animated.View>
</TouchableWithoutFeedback>
);
};
- Use a
<ScrollView horizontal>combined with a child<View>that has a fixedheightandflexWrap: 'wrap', flexDirection: 'column'. This forces children to form columns and flow horizontally. - Use
Animated.ViewandTouchableWithoutFeedbackto create the scale animation.
Jetpack Compose
@Composable
fun TileDesignScreen() {
LazyHorizontalGrid(
rows = GridCells.Fixed(2),
modifier = Modifier.fillMaxSize().background(Color(0xFF111111)),
contentPadding = PaddingValues(40.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
item(span = { GridItemSpan(1) }) { Tile("Mail", Color(0xFF0078D7)) }
// Note: LazyHorizontalGrid doesn't easily support spanning multiple rows (2x2 tiles).
// For a true Metro layout, you often have to build a custom Layout or use staggered grids.
item(span = { GridItemSpan(2) }) { Tile("Photos Wide", Color(0xFF00CC6A)) }
item(span = { GridItemSpan(1) }) { Tile("Weather", Color(0xFF2D7D9A)) }
}
}
@Composable
fun Tile(title: String, color: Color) {
var isPressed by remember { mutableStateOf(false) }
val scale by animateFloatAsState(if (isPressed) 0.95f else 1.0f)
Box(
modifier = Modifier
.size(150.dp) // Or wide/large based on params
.scale(scale)
.background(color) // Sharp corners! No RoundedCornerShape
.pointerInput(Unit) {
detectTapGestures(
onPress = {
isPressed = true
tryAwaitRelease()
isPressed = false
}
)
}
.padding(16.dp)
) {
// Icon
Box(modifier = Modifier.size(32.dp).background(Color.White.copy(alpha = 0.5f)).align(Alignment.TopStart))
// Text
Text(
text = title,
color = Color.White,
fontFamily = FontFamily.SansSerif,
modifier = Modifier.align(Alignment.BottomStart)
)
}
}
LazyHorizontalGridis the right tool, though building true 2x2 "Large" tiles requires custom layout math in Compose if mixing with 1x1 tiles.Modifier.scale()paired withpointerInputdetectTapGestureshandles the Metro interaction.
Do's and Don'ts
- DO: Place the tile label text strictly in the bottom-left corner of the tile.
- DON'T: Add drop shadows or gradients to the tiles.
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.
FAQ
Common questions
Discussion
Questions & comments ยท 0
Sign In Sign in to leave a comment.