Skip to main content
The DPM Delivery Mobile app uses a modern styling system combining HeroUI Native components, Uniwind (Tailwind CSS for React Native), and custom theme configuration.

Styling Stack

The app uses a multi-layered styling approach:

HeroUI Native

Pre-built, accessible UI components with consistent design

Uniwind

Tailwind CSS utility classes for React Native

Tailwind CSS 4

Modern CSS framework with custom theming

HeroUI Native Components

HeroUI Native provides accessible, themeable components built for React Native.

Installation

Already installed in the project:
package.json
{
  "dependencies": {
    "heroui-native": "^1.0.0-beta.12"
  }
}

Using HeroUI Components

Import components from heroui-native:
import { Button, TextField, Card } from "heroui-native";

function MyComponent() {
  return (
    <Card>
      <TextField>
        <TextField.Label>Email</TextField.Label>
        <TextField.Input placeholder="Enter email" />
      </TextField>
      <Button>Submit</Button>
    </Card>
  );
}

Available Components

  • TextField - Input fields with labels and validation
  • Button - Buttons with variants and states
  • Card - Container components
  • Select - Dropdown selectors
  • Modal - Dialog components
  • And many more…

Custom Input Component

The app provides a wrapped Input component with enhanced functionality:
components/ui/input.tsx
import {
  cn,
  TextField,
  type TextFieldInputProps,
  type TextFieldRootProps,
} from "heroui-native";
import { View, type TextInputProps } from "react-native";

interface Props {
  label?: string;
  placeholder?: string;
  description?: string;
  errorMessage?: string;
  prefix?: React.ReactNode;
  suffix?: React.ReactNode;
  labelClassName?: string;
}

export function Input(props: InputProps) {
  const {
    label,
    placeholder,
    errorMessage,
    isInvalid,
    prefix,
    suffix,
    className,
    ...inputProps
  } = props;
  
  return (
    <TextField isInvalid={isInvalid}>
      {label ? (
        <TextField.Label className={cn("text-sm font-medium")}>
          {label}
        </TextField.Label>
      ) : null}
      
      <View className="w-full flex-row items-center">
        <TextField.Input
          placeholder={placeholder}
          className={cn(
            "w-full border border-gray-300 h-12 shadow-none",
            prefix ? "pl-10" : "",
            suffix ? "pr-10" : "",
            className,
          )}
          {...inputProps}
        />
        {prefix ? <View className="absolute left-3.5">{prefix}</View> : null}
        {suffix ? <View className="absolute right-4">{suffix}</View> : null}
      </View>
      
      {errorMessage ? (
        <TextField.ErrorMessage>{errorMessage}</TextField.ErrorMessage>
      ) : null}
    </TextField>
  );
}
Usage:
import { Input } from "@/components/ui/input";
import { MaterialIcons } from "@expo/vector-icons";

function LoginForm() {
  return (
    <Input
      label="Email"
      placeholder="Enter your email"
      prefix={<MaterialIcons name="email" size={20} />}
      errorMessage="Email is required"
      isInvalid={hasError}
    />
  );
}

Uniwind Setup

Uniwind brings Tailwind CSS utility classes to React Native.

Configuration

Uniwind is configured in metro.config.js:
metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const { withUniwindConfig } = require("uniwind/metro");

const config = getDefaultConfig(__dirname);

module.exports = withUniwindConfig(config, {
  // CSS entry file
  cssEntryFile: "./src/global.css",
  // Auto-generated TypeScript types
  dtsFile: "./src/uniwind-types.d.ts",
});

TypeScript Types

Uniwind auto-generates TypeScript types in uniwind-types.d.ts:
src/uniwind-types.d.ts
/// <reference types="uniwind/types" />

declare module 'uniwind' {
  export interface UniwindConfig {
    themes: readonly ['light', 'dark']
  }
}
This provides full IntelliSense for Tailwind classes in your IDE.

Tailwind CSS Usage

Global Styles

Global styles are defined in src/global.css:
src/global.css
@import "tailwindcss";
@import "uniwind";
@import "heroui-native/styles";

@source "../node_modules/heroui-native/lib";

@layer theme {
  @variant light {
    --accent: oklch(71.24% 0.1848 37.77);
    --secondary: oklch(30.87% 0.0195 229.78);
  }
  
  @variant dark {
    --accent: oklch(71.24% 0.1848 37.77);
    --secondary: oklch(30.87% 0.0195 229.78);
  }
}

@theme static {
  --color-secondary: var(--secondary);
}

Using Tailwind Classes

Apply Tailwind classes using the className prop:
import { View, Text } from "react-native";

function Card() {
  return (
    <View className="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-md">
      <Text className="text-lg font-bold text-gray-900 dark:text-white">
        Shipment Details
      </Text>
      <Text className="text-sm text-gray-600 dark:text-gray-400 mt-2">
        Order #12345
      </Text>
    </View>
  );
}

Responsive Design

Tailwind breakpoints work in React Native:
<View className="p-4 sm:p-6 md:p-8">
  <Text className="text-base md:text-lg lg:text-xl">
    Responsive Text
  </Text>
</View>

Conditional Classes

Use cn() helper for conditional classes:
import { cn } from "heroui-native";

function Button({ variant, disabled }) {
  return (
    <Pressable
      className={cn(
        "px-4 py-2 rounded-lg",
        variant === "primary" && "bg-accent text-white",
        variant === "secondary" && "bg-gray-200 text-gray-800",
        disabled && "opacity-50"
      )}
    >
      <Text>Click Me</Text>
    </Pressable>
  );
}

Theme Configuration

Theme Constants

Theme colors and fonts are defined in src/constants/theme.ts:
src/constants/theme.ts
import { Platform } from 'react-native';

const tintColorLight = '#0a7ea4';
const tintColorDark = '#fff';

export const Colors = {
  light: {
    text: '#11181C',
    background: '#fff',
    tint: tintColorLight,
    icon: '#687076',
    tabIconDefault: '#687076',
    tabIconSelected: tintColorLight,
  },
  dark: {
    text: '#ECEDEE',
    background: '#151718',
    tint: tintColorDark,
    icon: '#9BA1A6',
    tabIconDefault: '#9BA1A6',
    tabIconSelected: tintColorDark,
  },
};

export const Fonts = Platform.select({
  ios: {
    sans: 'system-ui',
    serif: 'ui-serif',
    rounded: 'ui-rounded',
    mono: 'ui-monospace',
  },
  default: {
    sans: 'normal',
    serif: 'serif',
    rounded: 'normal',
    mono: 'monospace',
  },
  web: {
    sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
    serif: "Georgia, 'Times New Roman', serif",
    rounded: "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, sans-serif",
    mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
  },
});

Using Theme Colors

Access theme colors with the useThemeColor hook:
import { useThemeColor } from "@/hooks/use-theme-color";

function ThemedText(props) {
  const { style, lightColor, darkColor, ...rest } = props;
  const color = useThemeColor(
    { light: lightColor, dark: darkColor },
    'text'
  );

  return <Text style={[{ color }, style]} {...rest} />;
}
Usage:
<ThemedText>Default themed text</ThemedText>
<ThemedText lightColor="#000" darkColor="#fff">
  Custom themed text
</ThemedText>

Theme-Aware Components

The app provides theme-aware wrapper components:
components/themed-view.tsx
import { View, type ViewProps } from "react-native";
import { useThemeColor } from "@/hooks/use-theme-color";

export type ThemedViewProps = ViewProps & {
  lightColor?: string;
  darkColor?: string;
};

export function ThemedView(props: ThemedViewProps) {
  const { style, lightColor, darkColor, ...otherProps } = props;
  const backgroundColor = useThemeColor(
    { light: lightColor, dark: darkColor },
    'background'
  );

  return <View style={[{ backgroundColor }, style]} {...otherProps} />;
}

Styling Patterns

1. Component Styling

Combine HeroUI components with Tailwind classes:
import { Card } from "heroui-native";
import { View, Text } from "react-native";

function ShipmentCard({ shipment }) {
  return (
    <Card className="mb-4">
      <View className="p-4">
        <Text className="text-lg font-bold mb-2">
          {shipment.reference}
        </Text>
        <View className="flex-row items-center justify-between">
          <Text className="text-sm text-gray-600">
            {shipment.recipientName}
          </Text>
          <StatusBadge status={shipment.status} />
        </View>
      </View>
    </Card>
  );
}

2. Status Badges

Dynamic styling based on status:
utils/style.ts
import { ShipmentStatus } from "@/types/enums/shipment.enum";

export function getStatusColor(status: string) {
  switch (status) {
    case ShipmentStatus.IN_TRANSIT:
    case ShipmentStatus.OUT_FOR_DELIVERY:
      return "bg-accent/10";
    case ShipmentStatus.DELIVERED:
      return "bg-green-100/10";
    case ShipmentStatus.PENDING:
      return "bg-yellow-100/10";
    case ShipmentStatus.FAILED_DELIVERY_ATTEMPT:
      return "bg-red-100/10";
    default:
      return "bg-gray-100";
  }
}

export function getStatusTextColor(status: string) {
  switch (status) {
    case ShipmentStatus.IN_TRANSIT:
      return "text-accent";
    case ShipmentStatus.DELIVERED:
      return "text-green-600";
    case ShipmentStatus.PENDING:
      return "text-yellow-600";
    case ShipmentStatus.FAILED_DELIVERY_ATTEMPT:
      return "text-red-600";
    default:
      return "text-gray-600";
  }
}
Usage:
import { getStatusColor, getStatusTextColor } from "@/utils/style";

function StatusBadge({ status }) {
  return (
    <View className={cn("px-3 py-1 rounded-full", getStatusColor(status))}>
      <Text className={cn("text-xs font-medium", getStatusTextColor(status))}>
        {status}
      </Text>
    </View>
  );
}

3. Form Styling

Consistent form field styling:
import { Input } from "@/components/ui/input";
import { FormField } from "@/components/form-field";

function LoginForm() {
  const { control, handleSubmit } = useForm();

  return (
    <View className="space-y-4">
      <FormField
        control={control}
        name="phone"
        label="Phone Number"
        placeholder="Enter phone number"
        className="bg-white dark:bg-gray-800"
      />
      
      <FormField
        control={control}
        name="password"
        label="Password"
        placeholder="Enter password"
        secureTextEntry
        className="bg-white dark:bg-gray-800"
      />
    </View>
  );
}

4. Custom Theme Colors

Access custom theme colors from global.css:
<View className="bg-accent">
  <Text className="text-accent">
    Accent colored text
  </Text>
</View>

<View className="bg-secondary">
  <Text className="text-secondary">
    Secondary colored text
  </Text>
</View>

5. Dark Mode

Automatically handle dark mode with dark: prefix:
<View className="bg-white dark:bg-gray-900">
  <Text className="text-gray-900 dark:text-white">
    Automatically themed text
  </Text>
</View>

Platform-Specific Styles

Using Platform Select

import { Platform, StyleSheet } from "react-native";

const styles = StyleSheet.create({
  container: {
    padding: 16,
    ...Platform.select({
      ios: {
        shadowColor: '#000',
        shadowOffset: { width: 0, height: 2 },
        shadowOpacity: 0.1,
        shadowRadius: 4,
      },
      android: {
        elevation: 4,
      },
      web: {
        boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
      },
    }),
  },
});

Platform-Specific Files

Create platform-specific implementations:
components/
├── icon-symbol.tsx       # Default implementation
├── icon-symbol.ios.tsx   # iOS-specific
└── icon-symbol.web.tsx   # Web-specific

Best Practices

1. Prefer Tailwind Classes

// ✅ Good: Tailwind classes
<View className="flex-1 p-4 bg-white rounded-lg" />

// ❌ Avoid: Inline styles
<View style={{ flex: 1, padding: 16, backgroundColor: '#fff', borderRadius: 8 }} />

2. Use Theme Colors

// ✅ Good: Theme-aware colors
<Text className="text-accent dark:text-white" />

// ❌ Avoid: Hard-coded colors
<Text style={{ color: '#0a7ea4' }} />

3. Componentize Styles

// ✅ Good: Reusable styled component
const CardContainer = ({ children }) => (
  <View className="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-md">
    {children}
  </View>
);

// ❌ Avoid: Repeated classes
<View className="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-md">...</View>
<View className="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-md">...</View>

4. Use the cn() Helper

import { cn } from "heroui-native";

// ✅ Good: Conditional classes with cn()
className={cn(
  "base-classes",
  isActive && "active-classes",
  isDisabled && "disabled-classes"
)}

// ❌ Avoid: Template literals
className={`base-classes ${isActive ? 'active-classes' : ''}`}

Next Steps

Build docs developers (and LLMs) love