Flutter ClipRRect Localization: Rounded Corners for Multilingual Interfaces
ClipRRect is a Flutter widget that clips its child using a rounded rectangle. In multilingual applications, ClipRRect creates consistent, polished visual elements that frame content elegantly regardless of text length or direction.
Understanding ClipRRect in Localization Context
ClipRRect applies rounded corner clipping to any widget, creating smooth, modern UI elements. For multilingual apps, this enables:
- Consistent card and container styling across all languages
- Profile images and avatars that look uniform globally
- Button and input field borders that adapt to content
- Image galleries with rounded corners for any text overlay
Why ClipRRect Matters for Multilingual Apps
ClipRRect provides:
- Visual consistency: Rounded elements look identical in all locales
- Modern aesthetics: Contemporary design that transcends cultural boundaries
- Content framing: Clean borders around variable-length text
- Directional neutrality: Rounded corners work equally in LTR and RTL
Basic ClipRRect Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedClipRRectExample extends StatelessWidget {
const LocalizedClipRRectExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.all(20),
color: Theme.of(context).colorScheme.primaryContainer,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.welcomeTitle,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
const SizedBox(height: 8),
Text(
l10n.welcomeMessage,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
}
Card Components
Localized Content Card
class LocalizedContentCard extends StatelessWidget {
final String title;
final String description;
final String? imageUrl;
final VoidCallback? onTap;
const LocalizedContentCard({
super.key,
required this.title,
required this.description,
this.imageUrl,
this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Container(
color: Theme.of(context).colorScheme.surface,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (imageUrl != null)
ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(16),
),
child: Image.network(
imageUrl!,
height: 160,
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
description,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
),
),
);
}
}
class LocalizedCardGrid extends StatelessWidget {
const LocalizedCardGrid({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return GridView.count(
crossAxisCount: 2,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
padding: const EdgeInsets.all(16),
children: [
LocalizedContentCard(
title: l10n.articleOneTitle,
description: l10n.articleOneDesc,
imageUrl: 'https://example.com/image1.jpg',
),
LocalizedContentCard(
title: l10n.articleTwoTitle,
description: l10n.articleTwoDesc,
imageUrl: 'https://example.com/image2.jpg',
),
],
);
}
}
Directional Border Radius
class DirectionalClipRRect extends StatelessWidget {
final Widget child;
final double startRadius;
final double endRadius;
final double topRadius;
final double bottomRadius;
const DirectionalClipRRect({
super.key,
required this.child,
this.startRadius = 0,
this.endRadius = 0,
this.topRadius = 0,
this.bottomRadius = 0,
});
@override
Widget build(BuildContext context) {
final isRtl = Directionality.of(context) == TextDirection.rtl;
return ClipRRect(
borderRadius: BorderRadiusDirectional.only(
topStart: Radius.circular(isRtl ? endRadius : startRadius),
topEnd: Radius.circular(isRtl ? startRadius : endRadius),
bottomStart: Radius.circular(isRtl ? endRadius : startRadius),
bottomEnd: Radius.circular(isRtl ? startRadius : endRadius),
).resolve(Directionality.of(context)),
child: child,
);
}
}
class LocalizedChatBubble extends StatelessWidget {
final String message;
final bool isSent;
const LocalizedChatBubble({
super.key,
required this.message,
required this.isSent,
});
@override
Widget build(BuildContext context) {
final isRtl = Directionality.of(context) == TextDirection.rtl;
final bubbleAlignment = isSent
? (isRtl ? Alignment.centerLeft : Alignment.centerRight)
: (isRtl ? Alignment.centerRight : Alignment.centerLeft);
return Align(
alignment: bubbleAlignment,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.75,
),
child: ClipRRect(
borderRadius: BorderRadiusDirectional.only(
topStart: const Radius.circular(16),
topEnd: const Radius.circular(16),
bottomStart: Radius.circular(isSent ? 16 : 4),
bottomEnd: Radius.circular(isSent ? 4 : 16),
).resolve(Directionality.of(context)),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
color: isSent
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceVariant,
child: Text(
message,
style: TextStyle(
color: isSent
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
),
);
}
}
class LocalizedChatView extends StatelessWidget {
const LocalizedChatView({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return ListView(
padding: const EdgeInsets.all(16),
children: [
LocalizedChatBubble(
message: l10n.receivedMessage,
isSent: false,
),
const SizedBox(height: 8),
LocalizedChatBubble(
message: l10n.sentMessage,
isSent: true,
),
const SizedBox(height: 8),
LocalizedChatBubble(
message: l10n.receivedMessageTwo,
isSent: false,
),
],
);
}
}
Avatar and Profile Components
Rounded Profile Image
class LocalizedProfileAvatar extends StatelessWidget {
final String? imageUrl;
final String name;
final double size;
const LocalizedProfileAvatar({
super.key,
this.imageUrl,
required this.name,
this.size = 48,
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(size / 2),
child: Container(
width: size,
height: size,
color: Theme.of(context).colorScheme.primaryContainer,
child: imageUrl != null
? Image.network(
imageUrl!,
fit: BoxFit.cover,
)
: Center(
child: Text(
name.isNotEmpty ? name[0].toUpperCase() : '?',
style: TextStyle(
fontSize: size * 0.4,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
),
);
}
}
class LocalizedUserTile extends StatelessWidget {
const LocalizedUserTile({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return ListTile(
leading: LocalizedProfileAvatar(
name: l10n.userName,
size: 48,
),
title: Text(l10n.userName),
subtitle: Text(l10n.userStatus),
trailing: IconButton(
icon: const Icon(Icons.message),
onPressed: () {},
tooltip: l10n.sendMessageTooltip,
),
);
}
}
Profile Card with Rounded Image
class LocalizedProfileCard extends StatelessWidget {
const LocalizedProfileCard({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Container(
color: Theme.of(context).colorScheme.surface,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 100,
color: Theme.of(context).colorScheme.primary,
),
Transform.translate(
offset: const Offset(0, -40),
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(40),
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
border: Border.all(
color: Theme.of(context).colorScheme.surface,
width: 4,
),
borderRadius: BorderRadius.circular(40),
),
child: Icon(
Icons.person,
size: 40,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
const SizedBox(height: 8),
Text(
l10n.profileName,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 4),
Text(
l10n.profileBio,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
OutlinedButton(
onPressed: () {},
child: Text(l10n.followButton),
),
const SizedBox(width: 12),
FilledButton(
onPressed: () {},
child: Text(l10n.messageButton),
),
],
),
const SizedBox(height: 16),
],
),
),
],
),
),
);
}
}
Input Field Styling
Rounded Search Bar
class LocalizedSearchBar extends StatelessWidget {
final TextEditingController? controller;
final ValueChanged<String>? onChanged;
const LocalizedSearchBar({
super.key,
this.controller,
this.onChanged,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return ClipRRect(
borderRadius: BorderRadius.circular(28),
child: Container(
color: Theme.of(context).colorScheme.surfaceVariant,
child: TextField(
controller: controller,
onChanged: onChanged,
decoration: InputDecoration(
hintText: l10n.searchPlaceholder,
prefixIcon: const Icon(Icons.search),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
),
),
);
}
}
class LocalizedSearchHeader extends StatelessWidget {
const LocalizedSearchHeader({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
l10n.searchTitle,
style: Theme.of(context).textTheme.headlineMedium,
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: LocalizedSearchBar(),
),
],
);
}
}
Rounded Input Group
class LocalizedRoundedInputGroup extends StatelessWidget {
const LocalizedRoundedInputGroup({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Container(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Column(
children: [
TextField(
decoration: InputDecoration(
labelText: l10n.firstNameLabel,
border: InputBorder.none,
contentPadding: const EdgeInsets.all(16),
),
),
Divider(
height: 1,
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
),
TextField(
decoration: InputDecoration(
labelText: l10n.lastNameLabel,
border: InputBorder.none,
contentPadding: const EdgeInsets.all(16),
),
),
Divider(
height: 1,
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
),
TextField(
decoration: InputDecoration(
labelText: l10n.emailLabel,
border: InputBorder.none,
contentPadding: const EdgeInsets.all(16),
),
),
],
),
),
);
}
}
Image Gallery Components
Rounded Image Grid
class LocalizedImageGallery extends StatelessWidget {
final List<String> imageUrls;
final List<String> captions;
const LocalizedImageGallery({
super.key,
required this.imageUrls,
required this.captions,
});
@override
Widget build(BuildContext context) {
return GridView.builder(
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: imageUrls.length,
itemBuilder: (context, index) {
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Stack(
fit: StackFit.expand,
children: [
Image.network(
imageUrls[index],
fit: BoxFit.cover,
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent,
],
),
),
child: Text(
captions[index],
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
);
},
);
}
}
class LocalizedGallerySection extends StatelessWidget {
const LocalizedGallerySection({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
l10n.galleryTitle,
style: Theme.of(context).textTheme.titleLarge,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: LocalizedImageGallery(
imageUrls: const [
'https://example.com/1.jpg',
'https://example.com/2.jpg',
'https://example.com/3.jpg',
'https://example.com/4.jpg',
],
captions: [
l10n.imageCaption1,
l10n.imageCaption2,
l10n.imageCaption3,
l10n.imageCaption4,
],
),
),
],
);
}
}
Button Components
Pill-Shaped Buttons
class LocalizedPillButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
final bool isPrimary;
const LocalizedPillButton({
super.key,
required this.label,
this.onPressed,
this.isPrimary = true,
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(24),
child: Material(
color: isPrimary
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceVariant,
child: InkWell(
onTap: onPressed,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
child: Text(
label,
style: TextStyle(
color: isPrimary
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
),
),
),
);
}
}
class LocalizedButtonRow extends StatelessWidget {
const LocalizedButtonRow({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
LocalizedPillButton(
label: l10n.declineButton,
isPrimary: false,
onPressed: () {},
),
const SizedBox(width: 12),
LocalizedPillButton(
label: l10n.acceptButton,
isPrimary: true,
onPressed: () {},
),
],
);
}
}
Tag and Chip Components
Rounded Tags
class LocalizedTag extends StatelessWidget {
final String label;
final Color? color;
const LocalizedTag({
super.key,
required this.label,
this.color,
});
@override
Widget build(BuildContext context) {
final tagColor = color ?? Theme.of(context).colorScheme.primary;
return ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
color: tagColor.withOpacity(0.1),
child: Text(
label,
style: TextStyle(
color: tagColor,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
);
}
}
class LocalizedTagList extends StatelessWidget {
const LocalizedTagList({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
LocalizedTag(label: l10n.tagFlutter),
LocalizedTag(label: l10n.tagLocalization, color: Colors.green),
LocalizedTag(label: l10n.tagMobile, color: Colors.orange),
LocalizedTag(label: l10n.tagUI, color: Colors.purple),
],
);
}
}
ARB File Structure
English (app_en.arb)
{
"@@locale": "en",
"welcomeTitle": "Welcome",
"welcomeMessage": "We're glad to have you here. Explore our features and get started.",
"articleOneTitle": "Getting Started",
"articleOneDesc": "Learn the basics of Flutter localization.",
"articleTwoTitle": "Advanced Tips",
"articleTwoDesc": "Master complex localization patterns.",
"receivedMessage": "Hello! How can I help you today?",
"sentMessage": "Hi, I have a question about localization.",
"receivedMessageTwo": "Of course! I'd be happy to help with that.",
"userName": "John Doe",
"userStatus": "Online",
"sendMessageTooltip": "Send message",
"profileName": "Sarah Johnson",
"profileBio": "Flutter developer passionate about creating beautiful multilingual applications.",
"followButton": "Follow",
"messageButton": "Message",
"searchPlaceholder": "Search...",
"searchTitle": "Discover",
"firstNameLabel": "First Name",
"lastNameLabel": "Last Name",
"emailLabel": "Email",
"galleryTitle": "Photo Gallery",
"imageCaption1": "Beautiful sunset view",
"imageCaption2": "City skyline at night",
"imageCaption3": "Mountain landscape",
"imageCaption4": "Ocean waves",
"declineButton": "Decline",
"acceptButton": "Accept",
"tagFlutter": "Flutter",
"tagLocalization": "Localization",
"tagMobile": "Mobile",
"tagUI": "UI/UX"
}
German (app_de.arb)
{
"@@locale": "de",
"welcomeTitle": "Willkommen",
"welcomeMessage": "Wir freuen uns, Sie hier zu haben. Entdecken Sie unsere Funktionen und legen Sie los.",
"articleOneTitle": "Erste Schritte",
"articleOneDesc": "Lernen Sie die Grundlagen der Flutter-Lokalisierung.",
"articleTwoTitle": "Fortgeschrittene Tipps",
"articleTwoDesc": "Meistern Sie komplexe Lokalisierungsmuster.",
"receivedMessage": "Hallo! Wie kann ich Ihnen heute helfen?",
"sentMessage": "Hallo, ich habe eine Frage zur Lokalisierung.",
"receivedMessageTwo": "Natürlich! Ich helfe Ihnen gerne dabei.",
"userName": "Max Mustermann",
"userStatus": "Online",
"sendMessageTooltip": "Nachricht senden",
"profileName": "Anna Schmidt",
"profileBio": "Flutter-Entwicklerin mit Leidenschaft für schöne mehrsprachige Anwendungen.",
"followButton": "Folgen",
"messageButton": "Nachricht",
"searchPlaceholder": "Suchen...",
"searchTitle": "Entdecken",
"firstNameLabel": "Vorname",
"lastNameLabel": "Nachname",
"emailLabel": "E-Mail",
"galleryTitle": "Fotogalerie",
"imageCaption1": "Schöner Sonnenuntergang",
"imageCaption2": "Stadtsilhouette bei Nacht",
"imageCaption3": "Berglandschaft",
"imageCaption4": "Meereswellen",
"declineButton": "Ablehnen",
"acceptButton": "Annehmen",
"tagFlutter": "Flutter",
"tagLocalization": "Lokalisierung",
"tagMobile": "Mobil",
"tagUI": "UI/UX"
}
Arabic (app_ar.arb)
{
"@@locale": "ar",
"welcomeTitle": "مرحباً",
"welcomeMessage": "يسعدنا وجودك هنا. اكتشف ميزاتنا وابدأ.",
"articleOneTitle": "البدء",
"articleOneDesc": "تعلم أساسيات توطين Flutter.",
"articleTwoTitle": "نصائح متقدمة",
"articleTwoDesc": "أتقن أنماط التوطين المعقدة.",
"receivedMessage": "مرحباً! كيف يمكنني مساعدتك اليوم؟",
"sentMessage": "مرحباً، لدي سؤال حول التوطين.",
"receivedMessageTwo": "بالطبع! يسعدني مساعدتك في ذلك.",
"userName": "أحمد محمد",
"userStatus": "متصل",
"sendMessageTooltip": "إرسال رسالة",
"profileName": "سارة أحمد",
"profileBio": "مطورة Flutter شغوفة بإنشاء تطبيقات جميلة متعددة اللغات.",
"followButton": "متابعة",
"messageButton": "رسالة",
"searchPlaceholder": "بحث...",
"searchTitle": "اكتشف",
"firstNameLabel": "الاسم الأول",
"lastNameLabel": "اسم العائلة",
"emailLabel": "البريد الإلكتروني",
"galleryTitle": "معرض الصور",
"imageCaption1": "منظر غروب جميل",
"imageCaption2": "أفق المدينة ليلاً",
"imageCaption3": "منظر جبلي",
"imageCaption4": "أمواج المحيط",
"declineButton": "رفض",
"acceptButton": "قبول",
"tagFlutter": "فلاتر",
"tagLocalization": "التوطين",
"tagMobile": "الجوال",
"tagUI": "واجهة المستخدم"
}
Best Practices Summary
Do's
- Use consistent border radius values across your app for visual harmony
- Apply ClipRRect to images that need rounded corners
- Combine with Container for colored rounded backgrounds
- Use BorderRadiusDirectional for RTL-aware asymmetric corners
- Test chat bubbles and cards in both LTR and RTL layouts
Don'ts
- Don't overuse clipping as it can impact performance
- Don't clip text that might overflow - use proper text handling first
- Don't forget to match inner and outer radii when nesting rounded elements
- Don't assume corners look the same in all contexts - test thoroughly
Conclusion
ClipRRect is an essential widget for creating modern, polished interfaces in multilingual Flutter applications. By applying rounded corners consistently to cards, avatars, buttons, and input fields, you create a cohesive visual language that works across all locales. The widget's directional awareness through BorderRadiusDirectional ensures chat bubbles and asymmetric designs adapt correctly to RTL languages.