This guide covers styling patterns, best practices, and conventions for the Beyond-UI design system.
The style factory pattern is the standard way to create styles in Beyond-UI. It replaces StyleSheet.create() with pure functions that generate styles dynamically.
// Component.styles.ts
import type { ViewStyle, TextStyle } from 'react-native'
import { colors } from '../../tokens/colors'
import type { ThemeMode } from '../../tokens/colors'
export interface ComponentStyleConfig {
container: ViewStyle
content: TextStyle
}
export function getComponentStyles(
variant: Variant,
size: Size,
theme: ThemeMode
): ComponentStyleConfig {
const container: ViewStyle = {
padding: spacing[16],
backgroundColor: colors.bg[theme].default,
}
const content: TextStyle = {
fontSize: typography.body.fontSize,
color: colors.text[theme].primary,
}
return {
container,
content,
}
}
// Component.tsx
import { getComponentStyles } from './Component.styles'
import { useThemeContext } from '../../theme'
export function Component({ variant, size }: ComponentProps) {
const { theme } = useThemeContext()
const styles = getComponentStyles(variant, size, theme)
return (
<View style={styles.container}>
<Text style={styles.content}>Content</Text>
</View>
)
}
✅ Dynamic styling - Adapts to props and theme ✅ Type safety - Explicit return types ✅ Performance - No StyleSheet overhead ✅ Testability - Pure functions ✅ Consistency - Single pattern everywhere
// Primary colors
colors.primary[500] // #ff6633
colors.primary[400] // Lighter
colors.primary[600] // Darker
// Neutral colors
colors.gray[50] // Lightest
colors.gray[900] // Darkest
// Semantic colors
colors.error[500] // #e53e3e
colors.success[500] // #38a169
colors.warning[500] // #dd6b20
colors.info[500] // #3182ce
// Theme-aware colors
colors.text.light.primary // Light theme text
colors.text.dark.primary // Dark theme text
colors.bg[theme].default // Dynamic background
colors.border[theme].default // Dynamic border
colors.icon[theme].default // Dynamic icon
spacing[0] // 0px
spacing[2] // 2px
spacing[4] // 4px
spacing[6] // 6px
spacing[8] // 8px
spacing[10] // 10px
spacing[12] // 12px
spacing[16] // 16px
spacing[20] // 20px
spacing[24] // 24px
spacing[32] // 32px
spacing[40] // 40px
spacing[48] // 48px
spacing[64] // 64px
spacing[96] // 96px
// Headings
typography.h1 // 48px, bold
typography.h2 // 36px, bold
typography.h3 // 24px, semibold
typography.h4 // 20px, semibold
// Body text
typography.body // 16px, regular
typography.bodyMedium // 16px, medium
typography.small // 14px, regular
// Each includes: fontSize, fontWeight, lineHeight, letterSpacing, fontFamily
// Border radius
borderRadius.xxs // 2px
borderRadius.xs // 4px
borderRadius.s // 6px
borderRadius.m // 12px
borderRadius.l // 16px
borderRadius.xl // 24px
borderRadius.max // 9999px (pill shape)
// Border width
borderWidth.thin // 1px
borderWidth.medium // 2px
borderWidth.thick // 4px
// React Native shadows (object)
shadows.xs // Extra small
shadows.s // Small
shadows.m // Medium
shadows.l // Large
shadows.xl // Extra large
shadows.button // Button shadow
shadows.tabs // Tabs shadow
// Web-only box shadows (string)
boxShadows.xs // CSS box-shadow string
boxShadows.button // CSS box-shadow string
boxShadows.focusPrimary // Focus ring effect
DO:
// ✅ Always use tokens
const styles = {
padding: spacing[16],
fontSize: typography.body.fontSize,
color: colors.text[theme].primary,
borderRadius: borderRadius.m,
gap: spacing[12],
}
DON’T:
// ❌ Never hardcode values
const styles = {
padding: 16, // ❌ Use spacing[16]
fontSize: 16, // ❌ Use typography.body.fontSize
color: '#333', // ❌ Use colors.text[theme].primary
borderRadius: 12, // ❌ Use borderRadius.m
gap: 12, // ❌ Use spacing[12]
}
// Get theme from context
const { theme } = useThemeContext()
// Use theme-aware colors
const backgroundColor = colors.bg[theme].default
const textColor = colors.text[theme].primary
const borderColor = colors.border[theme].default
const iconColor = colors.icon[theme].default
// In style factory
export function getStyles(theme: ThemeMode) {
return {
container: {
backgroundColor: colors.bg[theme].default,
borderColor: colors.border[theme].default,
},
text: {
color: colors.text[theme].primary,
},
}
}
export function getStyles(theme: ThemeMode, variant: Variant) {
const isLight = theme === 'light'
// Simple condition
const backgroundColor = isLight
? colors.gray[50]
: colors.gray[900]
// Complex condition
const container: ViewStyle = {
backgroundColor: isLight ? colors.gray[50] : colors.gray[900],
borderColor: isLight ? colors.gray[200] : colors.gray[700],
}
// Variant-specific
if (variant === 'filled') {
container.backgroundColor = colors.primary[500]
}
return { container }
}
Most components follow a container + content structure:
interface ComponentStyleConfig {
container: ViewStyle // Outer wrapper
content: ViewStyle // Inner content area
text: TextStyle // Text styles
icon: ViewStyle // Icon wrapper
iconColor: string // Icon color (not a style object)
}
export function getStyles(theme: ThemeMode): ComponentStyleConfig {
return {
container: {
padding: spacing[16],
backgroundColor: colors.bg[theme].default,
},
content: {
gap: spacing[8],
},
text: {
fontSize: typography.body.fontSize,
color: colors.text[theme].primary,
},
icon: {
width: 24,
height: 24,
},
iconColor: colors.icon[theme].default,
}
}
Use configuration objects for size variants:
const sizeConfig = {
sm: {
height: 32,
paddingHorizontal: spacing[12],
paddingVertical: spacing[6],
fontSize: typography.small.fontSize,
iconSize: 16,
},
md: {
height: 40,
paddingHorizontal: spacing[16],
paddingVertical: spacing[8],
fontSize: typography.body.fontSize,
iconSize: 20,
},
lg: {
height: 48,
paddingHorizontal: spacing[20],
paddingVertical: spacing[12],
fontSize: typography.body.fontSize,
iconSize: 24,
},
} as const
export function getStyles(size: Size) {
const config = sizeConfig[size]
return {
container: {
height: config.height,
paddingHorizontal: config.paddingHorizontal,
paddingVertical: config.paddingVertical,
},
text: {
fontSize: config.fontSize,
},
iconSize: config.iconSize,
}
}
Handle interactive states in the factory:
export function getStyles(
isSelected: boolean,
isDisabled: boolean,
isHovered: boolean,
theme: ThemeMode
) {
const container: ViewStyle = {
backgroundColor: colors.bg[theme].default,
opacity: isDisabled ? 0.5 : 1,
}
// Selected state
if (isSelected) {
container.backgroundColor = colors.primary[500]
container.borderColor = colors.primary[600]
}
// Hover state
if (isHovered && !isDisabled) {
container.backgroundColor = colors.primary[400]
}
// Disabled state
if (isDisabled) {
container.cursor = 'not-allowed' as any
}
return { container }
}
// Horizontal row
const row: ViewStyle = {
flexDirection: 'row',
alignItems: 'center',
gap: spacing[12],
}
// Vertical column
const column: ViewStyle = {
flexDirection: 'column',
gap: spacing[16],
}
// Centered content
const centered: ViewStyle = {
justifyContent: 'center',
alignItems: 'center',
}
// Space between
const spaceBetween: ViewStyle = {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
}
// Use gap instead of margin for spacing
const container: ViewStyle = {
flexDirection: 'row',
gap: spacing[12], // ✅ Preferred
}
// Instead of:
const container: ViewStyle = {
flexDirection: 'row',
// ❌ Don't use margins between items
}
// Icon container
const iconContainer: ViewStyle = {
width: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
}
// Icon color as separate property
const iconColor = colors.icon[theme].default
// Usage in component
<View style={styles.iconContainer}>
<Icon size={24} color={styles.iconColor} />
</View>
// Full border
const bordered: ViewStyle = {
borderWidth: borderWidth.thin,
borderColor: colors.border[theme].default,
borderRadius: borderRadius.m,
}
// Bottom border only
const divider: ViewStyle = {
borderBottomWidth: borderWidth.thin,
borderBottomColor: colors.border[theme].default,
}
// Conditional border
if (variant === 'outlined') {
container.borderWidth = borderWidth.thin
container.borderColor = colors.border[theme].default
}
// React Native shadow
const elevated: ViewStyle = {
...shadows.m,
}
// Web box-shadow (use in web-only contexts)
const elevatedWeb: ViewStyle = {
boxShadow: boxShadows.m,
}
import { Platform } from 'react-native'
const interactive: ViewStyle = {
...(Platform.OS === 'web' && {
cursor: 'pointer' as any,
userSelect: 'none' as any,
}),
}
// ❌ BAD - Don't use StyleSheet.create
import { StyleSheet } from 'react-native'
const styles = StyleSheet.create({
container: {
padding: spacing[16],
}
})
// ✅ GOOD - Use style factory
export function getStyles(theme: ThemeMode) {
return {
container: {
padding: spacing[16],
}
}
}
// ❌ BAD - Don't use inline useMemo
const styles = useMemo(() => ({
container: {
backgroundColor: colors.bg[theme].default,
}
}), [theme])
// ✅ GOOD - Use style factory
const styles = getComponentStyles(theme)
// ❌ BAD - Hardcoded values
const container: ViewStyle = {
padding: 16, // ❌ Use spacing[16]
borderRadius: 8, // ❌ Use borderRadius.s
fontSize: 14, // ❌ Use typography.small.fontSize
color: '#333333', // ❌ Use colors.text[theme].primary
}
// ❌ BAD - Mixing StyleSheet and inline styles
const staticStyles = StyleSheet.create({ ... })
return (
<View style={[staticStyles.container, { backgroundColor: color }]} />
)
// ✅ GOOD - Pure style factory
const styles = getStyles(color, theme)
return <View style={styles.container} />
// ❌ BAD - What do these numbers mean?
const container: ViewStyle = {
width: 272, // ❌ What is this?
height: 856, // ❌ Why this value?
}
// ✅ GOOD - Use named constants or tokens
const SIDEBAR_WIDTH_EXPANDED = 272
const SIDEBAR_HEIGHT = 856
const container: ViewStyle = {
width: SIDEBAR_WIDTH_EXPANDED,
height: SIDEBAR_HEIGHT,
}
// ❌ OLD PATTERN
import { StyleSheet } from 'react-native'
const styles = StyleSheet.create({
container: {
padding: 16,
backgroundColor: '#fff',
},
text: {
fontSize: 16,
color: '#333',
},
})
export function Component() {
return (
<View style={styles.container}>
<Text style={styles.text}>Content</Text>
</View>
)
}
// ✅ NEW PATTERN
// Component.styles.ts
import type { ViewStyle, TextStyle } from 'react-native'
import { colors, spacing, typography } from '../../tokens'
import type { ThemeMode } from '../../tokens/colors'
export interface ComponentStyleConfig {
container: ViewStyle
text: TextStyle
}
export function getComponentStyles(theme: ThemeMode): ComponentStyleConfig {
return {
container: {
padding: spacing[16],
backgroundColor: colors.bg[theme].default,
},
text: {
fontSize: typography.body.fontSize,
color: colors.text[theme].primary,
},
}
}
// Component.tsx
import { getComponentStyles } from './Component.styles'
import { useThemeContext } from '../../theme'
export function Component() {
const { theme } = useThemeContext()
const styles = getComponentStyles(theme)
return (
<View style={styles.container}>
<Text style={styles.text}>Content</Text>
</View>
)
}
// ❌ OLD PATTERN
export function Component() {
const { theme } = useThemeContext()
return (
<View
style=
>
<Text
style=
>
Content
</Text>
</View>
)
}
// ✅ NEW PATTERN
// Component.styles.ts
export function getComponentStyles(theme: ThemeMode) {
return {
container: {
padding: spacing[16],
backgroundColor: colors.bg[theme].default,
},
text: {
color: colors.text[theme].primary,
},
}
}
// Component.tsx
export function Component() {
const { theme } = useThemeContext()
const styles = getComponentStyles(theme)
return (
<View style={styles.container}>
<Text style={styles.text}>Content</Text>
</View>
)
}
When creating or updating component styles, ensure:
.styles.ts fileStyleConfig interfaceStyleSheet.createuseMemo for stylesgap instead of margins where possiblePlatform.OS checks/packages/beyond-ui/stories/