SVRNTY Design System
A sophisticated Flutter design system with 20+ reusable components and a comprehensive server-driven token system. Build beautiful, consistent iOS-style applications with runtime theme switching across multiple brand identities.
Features
- ✨ 20+ Production-Ready Widgets - Organized by atomic design principles (atoms, molecules, organisms, layouts)
- 🎨 Server-Driven Design Tokens - Dynamic styling that fetches from .NET CQRS backend at runtime
- 🌓 Automatic Dark Mode - All colors and shadows adapt to light/dark themes
- 📱 Cupertino-Only - Pure iOS-style components, no Material widgets
- 🎯 8 Token Categories - Colors, spacing, typography, radii, shadows, gradients, animations, custom
- 🏢 Multi-Brand Support - Apple HIG, Svrnty, and PlanB brand identities
- 📐 Responsive Design - Built-in breakpoints and adaptive utilities
- 🔒 Type-Safe - Full null-safety with immutable data structures
- 💾 Offline Fallback - Static tokens keep your app running when server is unavailable
Table of Contents
- Installation
- Quick Start
- Architecture
- Components
- Design Tokens
- Usage Examples
- Responsive Design
- API Reference
- Best Practices
- Contributing
- License
Installation
1. Add Dependency
Add this to your Flutter app's pubspec.yaml:
dependencies:
svrnty_design_system:
path: ../svrnty_design_system # Adjust path as needed
2. Install Packages
flutter pub get
3. Import Library
import 'package:svrnty_design_system/svrnty_design_system.dart';
Quick Start
Basic Setup
import 'package:flutter/cupertino.dart';
import 'package:svrnty_design_system/svrnty_design_system.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Load style model from server
final loader = StyleModelLoader(
config: StyleModelApiConfig(baseUrl: 'http://localhost:6001'),
);
StyleModel styleModel;
try {
// Seed the model (idempotent)
await loader.seedAppleModel();
// Load the model
styleModel = await loader.loadModel('apple');
} catch (e) {
// Fallback to static tokens if server unavailable
styleModel = AppleTokens.appleModel;
}
runApp(MyApp(styleModel: styleModel));
}
class MyApp extends StatelessWidget {
final StyleModel styleModel;
const MyApp({required this.styleModel, super.key});
@override
Widget build(BuildContext context) {
return StyleModelProvider(
model: styleModel,
child: const CupertinoApp(
title: 'My App',
home: HomePage(),
),
);
}
}
Using Components
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
final model = StyleModelProvider.of(context);
return SvScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Home'),
),
child: SafeArea(
child: Padding(
padding: EdgeInsets.all(model.getSpacing('lg')),
child: Column(
children: [
// Use design system button
SvButton(
label: 'Continue',
onPressed: () => print('Pressed!'),
variant: SvButtonVariant.filled,
size: SvButtonSize.large,
),
SizedBox(height: model.getSpacing('md')),
// Use design system card
SvCard(
title: 'Welcome',
subtitle: 'Get started with SVRNTY Design System',
onTap: () => print('Card tapped'),
),
],
),
),
),
);
}
}
Architecture
Server-Driven Tokens
The design system uses a unique server-driven architecture:
┌─────────────────────────────────────────────────┐
│ Flutter App │
│ ┌───────────────────────────────────────────┐ │
│ │ StyleModelLoader │ │
│ │ ↓ │ │
│ │ POST /api/query/getStyleModel │ │
│ └───────────────────────────────────────────┘ │
└──────────────────┬──────────────────────────────┘
│ HTTP/JSON
↓
┌─────────────────────────────────────────────────┐
│ .NET CQRS Backend │
│ ┌───────────────────────────────────────────┐ │
│ │ PostgreSQL Database │ │
│ │ - StyleModel (id, name, version) │ │
│ │ - StyleToken (category, key, valueJson) │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────┐
│ Flutter App (Runtime) │
│ ┌───────────────────────────────────────────┐ │
│ │ StyleModelProvider │ │
│ │ - Makes tokens available to widget tree │ │
│ └───────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────┐ │
│ │ Widgets (SvButton, SvCard, etc.) │ │
│ │ - Access tokens by KEY, not value │ │
│ │ - model.getColor('systemBlue', context) │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Benefits:
- 🔄 Update styling without recompiling apps
- 🎨 Support multiple brands with same widgets
- 💾 Automatic fallback to static tokens when offline
- 🌓 Seamless light/dark mode switching
Components
Overview
20 production-ready components organized by atomic design:
| Category | Count | Components |
|---|---|---|
| Atoms | 3 | Button, TextField, Chip |
| Molecules | 8 | Card, ListTile, Dropdown, ExpansionTile, FormRow, ColorPicker, WizardProgress, ReorderableList |
| Organisms | 7 | Dialog, EmptyState, GlassContainer, PageHeader, Pagination, DataTable, TagCard |
| Layouts | 1 | Scaffold |
| Utils | 1 | Responsive (Breakpoints, Builder, Layout) |
Atoms
Simple, reusable building blocks:
SvButton
SvButton(
label: 'Continue',
onPressed: () {},
variant: SvButtonVariant.filled, // filled, outlined, plain, destructive
size: SvButtonSize.large, // small, medium, large
isExpanded: true, // Full width
isLoading: false, // Loading state
icon: CupertinoIcons.add, // Optional icon
)
SvTextField
SvTextField(
label: 'Email',
placeholder: 'Enter your email',
helperText: 'We\'ll never share your email',
errorText: isError ? 'Invalid email' : null,
leadingIcon: CupertinoIcons.mail,
onChanged: (value) => print(value),
)
SvChip
SvChip(
label: 'Active',
isSelected: true,
variant: SvChipVariant.filter, // filter, choice
onSelected: (selected) => print(selected),
)
Molecules
Composite components:
SvCard
SvCard(
title: 'Card Title',
subtitle: 'Card subtitle text',
trailing: Icon(CupertinoIcons.chevron_right),
onTap: () => print('Tapped'),
)
SvListTile
SvListTile(
title: 'Settings',
subtitle: 'Configure your preferences',
leading: Icon(CupertinoIcons.settings),
trailing: Icon(CupertinoIcons.chevron_right),
showDisclosure: true,
onTap: () {},
)
SvDropdown
SvDropdown<String>(
value: selectedValue,
items: ['Option 1', 'Option 2', 'Option 3'],
onChanged: (value) => setState(() => selectedValue = value),
itemBuilder: (item) => Text(item),
)
Organisms
Complex, feature-rich components:
SvConfirmDialog
showCupertinoDialog(
context: context,
builder: (context) => SvConfirmDialog(
title: 'Delete Account',
message: 'This action cannot be undone.',
confirmText: 'Delete',
cancelText: 'Cancel',
isDestructive: true,
onConfirm: () => deleteAccount(),
),
)
SvEmptyState
SvEmptyState(
icon: CupertinoIcons.folder,
title: 'No Items',
message: 'You haven\'t added any items yet.',
actionLabel: 'Add Item',
onAction: () => addItem(),
)
SvDataTable
SvDataTable(
columns: ['Name', 'Email', 'Role'],
rows: [
['John Doe', 'john@example.com', 'Admin'],
['Jane Smith', 'jane@example.com', 'User'],
],
sortColumnIndex: 0,
sortAscending: true,
onSort: (columnIndex, ascending) {},
)
Layouts
SvScaffold
SvScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Page Title'),
),
useSliverNavigationBar: false, // Use sliver for large titles
bottomBar: Container(/* ... */), // Optional bottom bar
child: /* Your content */,
)
Full Component List
See TOKENS.md for complete component documentation.
Design Tokens
Token Categories
The design system provides 8 token categories:
- Colors - 100+ adaptive color tokens (light/dark variants)
- Spacing - 8 spacing values (2pt → 48pt)
- Typography - 27 text styles (Apple HIG + Material Design 3)
- Radii - 9 border radius values (2pt → 9999pt)
- Shadows - 4 elevation levels (sm → xl)
- Gradients - Linear gradients for backgrounds
- Animations - 4 duration presets (fast → verySlow)
- Custom - Extensible custom token categories
Accessing Tokens
// Get StyleModel
final model = StyleModelProvider.of(context);
// Colors (adaptive to light/dark mode)
final color = model.getColor('systemBlue', context);
// Spacing
final spacing = model.getSpacing('md'); // 12.0 or 16.0
final padding = model.getPaddingAll('lg'); // EdgeInsets.all(16 or 20)
// Typography
final textStyle = model.getTextStyle('body'); // TextStyle(...)
// Radii
final radius = model.getRadius('md'); // 10.0 or 12.0
final borderRadius = model.getBorderRadius('lg'); // BorderRadius.circular(...)
// Shadows (adaptive)
final shadow = model.getShadow('md', context); // List<BoxShadow>
// Gradients
final gradient = model.getLinearGradient('brandPrimary');
// Animations
final duration = model.getAnimationDuration('normal'); // Duration(milliseconds: 250)
Context Extensions (Shorthand)
// Instead of StyleModelProvider.of(context)
final color = context.styleColor('systemBlue');
final spacing = context.styleSpacing('md');
final textStyle = context.styleTextStyle('body');
final padding = context.stylePaddingAll('lg');
final shadow = context.styleShadow('md');
// Responsive
final isMobile = context.isMobile;
final isTablet = context.isTablet;
final padding = context.adaptivePadding; // 16/24/32 based on screen size
Complete Token Reference
See TOKENS.md for a comprehensive list of all available tokens with values and usage examples.
Usage Examples
Example 1: Custom Button with Tokens
class CustomButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
final bool isPrimary;
const CustomButton({
required this.label,
this.onPressed,
this.isPrimary = true,
super.key,
});
@override
Widget build(BuildContext context) {
final model = StyleModelProvider.of(context);
return GestureDetector(
onTap: onPressed,
child: Container(
padding: model.getPaddingSymmetric(
horizontal: 'xl',
vertical: 'md',
),
decoration: BoxDecoration(
color: isPrimary
? model.getColor('systemBlue', context)
: model.getColor('systemGray5', context),
borderRadius: model.getBorderRadius('md'),
boxShadow: model.getShadow('sm', context),
),
child: Text(
label,
style: model.getTextStyle('body').copyWith(
color: isPrimary
? CupertinoColors.white
: model.getColor('label', context),
),
),
),
);
}
}
Example 2: Form with Validation
class LoginForm extends StatefulWidget {
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
String? _emailError;
String? _passwordError;
void _submit() {
setState(() {
_emailError = _validateEmail(_emailController.text);
_passwordError = _validatePassword(_passwordController.text);
});
if (_emailError == null && _passwordError == null) {
// Proceed with login
}
}
String? _validateEmail(String value) {
if (value.isEmpty) return 'Email is required';
if (!value.contains('@')) return 'Invalid email';
return null;
}
String? _validatePassword(String value) {
if (value.isEmpty) return 'Password is required';
if (value.length < 6) return 'Password must be at least 6 characters';
return null;
}
@override
Widget build(BuildContext context) {
final model = StyleModelProvider.of(context);
return SvScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Login'),
),
child: SafeArea(
child: Padding(
padding: EdgeInsets.all(model.getSpacing('lg')),
child: Column(
children: [
SvTextField(
label: 'Email',
placeholder: 'Enter your email',
controller: _emailController,
errorText: _emailError,
leadingIcon: CupertinoIcons.mail,
),
SizedBox(height: model.getSpacing('md')),
SvTextField(
label: 'Password',
placeholder: 'Enter your password',
controller: _passwordController,
errorText: _passwordError,
obscureText: true,
leadingIcon: CupertinoIcons.lock,
),
SizedBox(height: model.getSpacing('xl')),
SvButton(
label: 'Login',
onPressed: _submit,
variant: SvButtonVariant.filled,
size: SvButtonSize.large,
isExpanded: true,
),
],
),
),
),
);
}
}
Example 3: Responsive Layout
class ResponsivePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SvResponsiveLayout(
mobile: MobileLayout(),
tablet: TabletLayout(),
desktop: DesktopLayout(),
);
}
}
class MobileLayout extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SvScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Mobile'),
),
child: SafeArea(
child: Padding(
padding: context.adaptivePadding, // 16pt
child: Column(
spacing: context.styleSpacing('md'),
children: [
Text('Mobile View', style: context.styleTextStyle('largeTitle')),
SvCard(title: 'Card 1'),
SvCard(title: 'Card 2'),
],
),
),
),
);
}
}
Example 4: Animated Transitions
class AnimatedCard extends StatefulWidget {
@override
State<AnimatedCard> createState() => _AnimatedCardState();
}
class _AnimatedCardState extends State<AnimatedCard> {
bool isExpanded = false;
@override
Widget build(BuildContext context) {
final model = StyleModelProvider.of(context);
return GestureDetector(
onTap: () => setState(() => isExpanded = !isExpanded),
child: AnimatedContainer(
duration: model.getAnimationDuration('normal'),
curve: Curves.easeInOut,
height: isExpanded ? 200 : 100,
padding: model.getPaddingAll('lg'),
decoration: BoxDecoration(
color: model.getColor('secondarySystemBackground', context),
borderRadius: model.getBorderRadius('lg'),
boxShadow: model.getShadow(isExpanded ? 'lg' : 'sm', context),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Tap to expand', style: model.getTextStyle('headline')),
if (isExpanded) ...[
SizedBox(height: model.getSpacing('md')),
Text(
'Additional content appears here',
style: model.getTextStyle('body'),
),
],
],
),
),
);
}
}
Responsive Design
Breakpoints
Built-in responsive breakpoints:
| Breakpoint | Width | Usage |
|---|---|---|
mobile |
< 600px | Phones |
mobileSmall |
< 360px | Small phones |
mobileLarge |
480px+ | Large phones |
tablet |
600px - 1024px | Tablets |
tabletLarge |
840px+ | Large tablets |
desktop |
1024px+ | Desktops |
desktopLarge |
1920px+ | Large monitors |
desktopUltra |
2560px+ | Ultra-wide monitors |
Responsive Utilities
// Check device type
if (context.isMobile) {
// Mobile-specific layout
}
// Get screen dimensions
final width = context.screenWidth;
final height = context.screenHeight;
// Adaptive values
final columns = context.gridColumns; // 1-12 columns based on width
final padding = context.adaptivePadding; // 16/24/32
// Adaptive value selection
final fontSize = context.adaptive(
mobile: 14.0,
tablet: 16.0,
desktop: 18.0,
);
// Responsive builder
SvResponsiveBuilder(
builder: (context, deviceType) {
if (deviceType == DeviceType.mobile) {
return MobileWidget();
} else if (deviceType == DeviceType.tablet) {
return TabletWidget();
} else {
return DesktopWidget();
}
},
);
// Responsive layout switcher
SvResponsiveLayout(
mobile: MobileWidget(),
tablet: TabletWidget(),
desktop: DesktopWidget(),
);
API Reference
StyleModel
Core token container with comprehensive token access methods. See TOKENS.md for complete API documentation.
StyleModelProvider
InheritedWidget for providing tokens:
class StyleModelProvider extends InheritedWidget {
final StyleModel model;
static StyleModel of(BuildContext context);
static StyleModel? maybeOf(BuildContext context);
}
// Context extensions
extension StyleModelContext on BuildContext {
StyleModel get styleModel;
StyleModel? get maybeStyleModel;
Color styleColor(String key);
double styleSpacing(String key);
TextStyle styleTextStyle(String key);
// ... and more
}
StyleModelLoader
Server token loader:
class StyleModelLoader {
StyleModelLoader({required StyleModelApiConfig config});
Future<StyleModel> loadModel(String name);
Future<List<StyleModelSummary>> listModels();
// Seed commands (for development)
Future<void> seedAppleModel();
Future<void> seedSvrntyModel();
Future<void> seedPlanBModel();
}
class StyleModelApiConfig {
final String baseUrl;
final Map<String, String>? headers;
}
Best Practices
✅ DO
-
Use tokens instead of hardcoded values
// Good color: model.getColor('systemBlue', context) padding: model.getPaddingAll('md') // Bad color: Color(0xFF007AFF) padding: EdgeInsets.all(12) -
Pass context for adaptive tokens
// Good - adapts to light/dark mode color: model.getColor('label', context) // Bad - doesn't adapt color: model.getColorLight('label') -
Use semantic colors for states
// Good color: model.getColor('error', context) // Avoid (less semantic) color: model.getColor('systemRed', context) -
Wrap app with StyleModelProvider
CupertinoApp( builder: (context, child) { return StyleModelProvider( model: styleModel, child: child!, ); }, ); -
Use context extensions for cleaner code
// Cleaner Text('Hello', style: context.styleTextStyle('body')) // More verbose Text('Hello', style: StyleModelProvider.of(context).getTextStyle('body'))
❌ DON'T
- Don't hardcode values
- Don't skip context for adaptive tokens
- Don't mix brand token sets (use one consistently)
- Don't access tokens before wrapping with StyleModelProvider
- Don't import Material widgets (use Cupertino instead)
Contributing
Development Setup
- Clone the repository
- Install dependencies:
flutter pub get - Run theme browser:
cd ../theme_browser_app && flutter run
Adding a New Widget
-
Create widget in appropriate category:
lib/src/atoms|molecules|organisms/{widget_name}/ ├── sv_{widget_name}.dart └── README.md (optional) -
Export from main library file:
// lib/svrnty_design_system.dart export 'src/atoms/{widget_name}/sv_{widget_name}.dart'; -
Use tokens, never hardcode:
final model = StyleModelProvider.of(context); color: model.getColor('systemBlue', context) -
Test in theme browser app
-
Update version in
pubspec.yaml -
Document in README
Adding a New Token
Server approach (recommended):
- Add to database via backend API
- Apps fetch at startup
Fallback approach:
- Add to
lib/src/tokens/apple_tokens.dart(or svrnty/planb) - Update TOKENS.md
Project Structure
svrnty_design_system/
├── lib/
│ ├── svrnty_design_system.dart # Main export
│ └── src/
│ ├── core/ # StyleModel, Provider, Loader
│ ├── tokens/ # Fallback token sets
│ ├── atoms/ # Simple components (3)
│ ├── molecules/ # Composite components (8)
│ ├── organisms/ # Complex components (7)
│ ├── layouts/ # Layout components (1)
│ └── utils/ # Utilities
├── test/ # Unit tests
├── pubspec.yaml
├── README.md # This file
├── TOKENS.md # Token reference
└── CHANGELOG.md # Version history
Troubleshooting
"No StyleModel found in context"
Problem: Widget trying to access tokens before StyleModelProvider is initialized.
Solution: Ensure your app is wrapped with StyleModelProvider:
StyleModelProvider(
model: styleModel,
child: CupertinoApp(...),
)
"Failed to load style model from server"
Problem: Backend server is unavailable or not running.
Solutions:
- Start the backend:
cd backend-server/dotnet-cqrs && dotnet run --project Svrnty.Sample - Use fallback tokens:
styleModel = AppleTokens.appleModel - Check
baseUrlinStyleModelApiConfig
"Token key not found"
Problem: Accessing a token that doesn't exist.
Solution: Check available tokens:
print(model.colorKeys); // List all color tokens
print(model.spacingKeys); // List all spacing tokens
print(model.categories); // List all categories
See TOKENS.md for complete token reference.
Colors not adapting to dark mode
Problem: Not passing context to getColor().
Solution:
// Wrong
model.getColor('systemBlue')
// Correct
model.getColor('systemBlue', context)
Versioning
This package follows Semantic Versioning:
- MAJOR version for breaking changes
- MINOR version for new features (backward compatible)
- PATCH version for bug fixes
Current version: 1.0.0
See CHANGELOG.md for version history.
License
Proprietary - SVRNTY/PlanB Ecosystem
Copyright © 2025 Svrnty. All rights reserved.
Support
For issues, questions, or contributions:
- Issues: Create an issue in the repository
- Documentation: See TOKENS.md for token reference
- Example: Check
theme_browser_appdirectory - Architecture: See main repository CLAUDE.md
Acknowledgments
- Built with Flutter
- Inspired by Apple Human Interface Guidelines
- Follows Atomic Design methodology
Last Updated: 2025-12-29 Design System Version: 1.0.0
Libraries
- svrnty_design_system
- SVRNTY Design System