Overview
Positeasy / UI Guidelines

UI Design Guidelines

Standards for building consistent, accessible, and maintainable UI across all Positeasy products — POS, FoodBojo, and Payiteasy — using Material UI and Tailwind CSS.

Primary stack
MUI
+ Tailwind CSS
Max file length
200
lines per file
Breakpoints
3
mobile · tablet · desktop
Goals
Maintain consistent UI across all products, improve code readability, encourage reusable components, and eliminate duplicate UI code.
Scope
All components in src/components, pages, sections, shared layouts, hooks, services, and constants.
Non-negotiables
No inline style attributesNo <div> in MUI contextsNo generic component namesNo inline arrow functions in renderNo raw h1–h6 tagsNo pixel spacingNo hardcoded hex colors

Folder structure

All components must follow this directory layout. Consistency in structure makes onboarding and code reviews faster.

public/
  └── assets/
       ├── images/
       └── videos/

src/
  ├── components/
  │    ├── UI/           ← shared UI primitives
  │    ├── Layout/       ← PageHeader, SectionWrapper, etc.
  │    └── States/       ← EmptyState, ErrorState, LoadingState
  ├── pages/
  │    └── {RoutePage}.jsx
  ├── sections/
  │    └── {RoutePage}/
  │         ├── Views/
  │         ├── Forms/
  │         └── index.jsx
  ├── hooks/        ← e.g. useDebounce.js
  ├── utils/        ← pure reusable functions (no side-effects)
  ├── helpers/      ← feature-specific supporting functions
  ├── global/
  │    └── {RoutePage}.jsx
  ├── constants/    ← fixed values, app-wide
  ├── services/
  │    └── {RoutePage}.jsx
  └── layouts/      ← page-structure wrappers
utils vs helpers
utils/ contains pure, stateless functions with no side-effects — usable anywhere. helpers/ contains feature-specific logic tied to a particular domain or route. When in doubt: if it could be extracted to an npm package, it's a util.

Naming rules

Consistent naming makes components predictable and searchable. Everyone should be able to guess the file name from the component name.

PascalCase
Components, variable names, function names.
UserCard · InvoiceTable · handleSubmit
lowercase
Folder names only.
components/ · hooks/ · utils/
UPPER_SNAKE_CASE
Constants only.
MAX_RETRY_COUNT · API_BASE_URL
Boolean prefix
Must start with IS_, HAS_, CAN_, or SHOULD_.
IS_LOADING · HAS_PERMISSION
Suffix-based naming
Components must include a descriptive suffix that communicates intent at a glance.
UserCardInvoiceTableLoginFormAddItemDialogProductListOrderSectionDashboardView
Forbidden generic names
Never use names that reveal nothing about what the component does.
ComponentBoxItemWrapperContainer

Spacing & color

Use theme-aware tokens. Never hardcode pixel values or hex colors.

Spacing system
Both MUI and Tailwind use a multiplier system. Never use raw px values for margin or padding.
// MUI (1 unit = 8px)
sx={{ mt: 2 }}   → 16px
sx={{ p: 2 }}    → 16px

// Tailwind (1 unit = 4px)
className="mt-4" → 16px
className="p-4"  → 16px
Use
sx={{ mt: 2 }}
className="mt-4"
Avoid
style={{ marginTop: '16px' }}
Color usage
Always use semantic color tokens — never hardcode hex values in component files.
// MUI theme tokens
theme.palette.primary.main   →  color="primary"
theme.palette.error.main     →  color="error"
theme.palette.success.main   →  color="success"

// Tailwind classes
className="bg-mt-primary"
className="bg-mt-error"
className="bg-mt-success"

Typography

Always use MUI's Typography component. Raw heading tags break theme inheritance and create inconsistent sizing.

Typography over HTML tags
Use
<Typography variant="h5">User List</Typography>
Avoid
<h1>User List</h1>
Page header pattern
Every page must use this exact pattern for the header row.
<Stack direction="row" justifyContent="space-between" alignItems="center">
  <Typography variant="h5">Users</Typography>
  <Button variant="contained">Add User</Button>
</Stack>

MUI vs Tailwind

Both are allowed. This rule removes ambiguity so every engineer makes the same call every time.

Use MUI sx when

Building with MUI primitives — Box, Stack, Grid, Typography, Button, Dialog, Card, TextField.

You need theme-aware colorscolor="primary", color="error".

You need responsive breakpoints via Grid xs, sm, md props.

sx={{ mt: 2 }}color="primary"
Use Tailwind className when

Styling a non-MUI element — plain divs, custom layouts outside the MUI tree.

You need quick utility classes not available as sx props.

Building a page-level layout that doesn't depend on MUI Grid.

className="flex gap-2"className="mt-4"
Never mix on the same element
Use
<Box sx={{ mt: 2, p: 2 }}>

<div className="mt-4 p-4">
Avoid
<Box className="mt-4" sx={{ p: 2 }}>
mixing creates unpredictable CSS specificity
Spacing unit conversion
// MUI unit = 8px | Tailwind unit = 4px
sx={{ mt: 1 }}  =  mt-2   =  8px
sx={{ mt: 2 }}  =  mt-4   =  16px
sx={{ mt: 3 }}  =  mt-6   =  24px
sx={{ p: 2  }}  =  p-4    =  16px
sx={{ p: 3  }}  =  p-6    =  24px

Shared component index

Before building a new component, check this list. If it already exists, use and extend it. If you add a shared component, register it here.

If a component is used in 2+ sections it belongs in components/. Never duplicate — extend with an optional prop instead.
PageHeader
components/Layout/PageHeader.jsx
Title + action button row for every page.
titleactionLabelonAction
SectionWrapper
components/Layout/SectionWrapper.jsx
Padded Box with consistent section spacing.
childrensx
PageContainer
components/Layout/PageContainer.jsx
Max-width wrapper with standard page padding.
children
EmptyState
components/States/EmptyState.jsx
Required for every table/list with no data.
icontitlemessage
ErrorState
components/States/ErrorState.jsx
API failure state with retry action.
messageonRetry
LoadingState
components/States/LoadingState.jsx
Skeleton loader for data-loading regions.
rowsvariant
ConfirmDialog
components/UI/ConfirmDialog.jsx
Standard confirm/cancel for destructive actions.
opentitleonConfirmonClose
StatusChip
components/UI/StatusChip.jsx
Maps status string to theme color chip.
statuslabel
DataTable
components/UI/DataTable.jsx
Table with built-in loading, empty, error states.
columnsrowsloading

Forms

Forms use MUI Grid or Stack with react-hook-form. Every input must have a label, validation, helper text, and an error state.

Grid layout
<Grid container spacing={2}>
  <Grid item xs={6}>
    <TextField
      label="Name"
      fullWidth
      {...register('name')}
      error={!!errors.name}
      helperText={errors.name?.message}
    />
  </Grid>
</Grid>
Loading button on submit
<LoadingButton loading={isSubmitting} variant="contained">Save</LoadingButton>
Labeled inputs only
Use
<TextField label="Email" />
Avoid
<TextField />

Dialogs

All dialogs must follow the standard structure. Never render a dialog without maxWidth and fullWidth.

Required structure
<Dialog maxWidth="sm" fullWidth>
  <DialogTitle>Confirm Action</DialogTitle>
  <DialogContent>
    {/* content */}
  </DialogContent>
  <DialogActions>
    <Button>Cancel</Button>
    <Button variant="contained">Confirm</Button>
  </DialogActions>
</Dialog>
Width standard
Default to maxWidth="sm". Use "md" or "lg" only when content explicitly requires more space.

Cards & layout

Use MUI Card and Stack. Avoid raw div elements inside MUI contexts.

Card structure
<Card>
  <CardHeader title="..." />
  <CardContent>{/* body */}</CardContent>
  <CardActions>
    <Button>Action</Button>
  </CardActions>
</Card>
Box over div
Use
<Box> · <Stack>
Avoid
<div>
File length limit
A single file must not exceed 200 lines. Split large components into sub-components or extract logic into hooks.

Buttons & icons

Buttons must be intentional and descriptive. Icons must come from the library — never inline SVG.

Button variants
Default to variant="contained" with color="primary" for primary actions.
Use
<Button>Save Changes</Button>
Avoid
<Button>Click</Button>
Icon buttons
<Button startIcon={<AddIcon />}>Add User</Button>

<IconButton aria-label="delete">
  <DeleteIcon />
</IconButton>
No inline functions in render
Use
const handleClick = () => {}
<Button onClick={handleClick}>
Avoid
<Button onClick={() => handleClick(id)}>

States & feedback

Every data-fetching component must handle the full lifecycle. Shipping without all five states is incomplete.

Required API states
All components that load data must handle all five:
loadingsuccesserroremptydisabled
Empty state requirement
if (!data.length) {
  return <EmptyState />
}

Validation standard

All forms use react-hook-form with Yup schemas. No other validation approach is permitted.

Required libraries
react-hook-formyup@hookform/resolvers
Standard form setup
import { useForm } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import * as Yup from 'yup'

const schema = Yup.object({
  name:  Yup.string().required('Name is required'),
  email: Yup.string().email('Enter a valid email').required('Email is required'),
  phone: Yup.string().matches(/^[0-9]{10}$/, 'Enter a valid 10-digit number'),
})

const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm({
  resolver: yupResolver(schema),
  defaultValues: { name: '', email: '', phone: '' },
})
TextField error binding
<TextField
  label="Email"
  fullWidth
  {...register('email')}
  error={!!errors.email}
  helperText={errors.email?.message}
/>
Error message format — plain, human-readable, sentence-cased
Use
"Name is required"
"Enter a valid email"
"Must be at least 6 characters"
Avoid
"name_required"
"Invalid email format"
"String must be min 6"
Schema file location
Yup schemas must live alongside the form, not inline in the component file.
sections/{RoutePage}/Forms/{FormName}.schema.js   ← schema here
sections/{RoutePage}/Forms/{FormName}.jsx         ← imports schema

Performance

Apply React's memoization APIs to prevent unnecessary re-renders. Use only where the cost justifies it.

React.memo
Wrap components that receive stable props. Skips re-render when props haven't changed.
useMemo
Wrap expensive computed values. Only use when the computation is genuinely costly.
useCallback
Wrap functions passed as props or in dependency arrays to maintain referential stability.

Responsive rules

Every component must function correctly at all three breakpoints. No fixed widths.

Required breakpoints
MobileTabletDesktop
Use MUI Grid props (xs, sm, md) or Tailwind responsive prefixes. Never hard-code fixed widths.

PR review checklist

Run through this before marking a PR ready for review. Reviewers should reject PRs that fail these checks.

0 of 20 checked
  • Component name uses PascalCase with a descriptive suffix (Card, Table, Form, Dialog)
  • No generic names — Component, Box, Item, Wrapper, Container are forbidden
  • Folders are lowercase, constants are UPPER_SNAKE_CASE, booleans use IS_/HAS_/CAN_/SHOULD_ prefix
  • File is under 200 lines — logic extracted to hooks or sub-components if over
  • Checked the component index — no duplicate of an existing shared component
  • Uses Box/Stack instead of raw div in MUI contexts
  • Uses Typography instead of h1–h6 or p tags
  • Icons from the icon library — no inline SVG paths
  • Form uses react-hook-form + yupResolver — no manual state for values or errors
  • Yup schema is in a separate .schema.js file, not inline in the component
  • Every TextField has label, error, and helperText bound from formState
  • Submit button uses LoadingButton with isSubmitting
  • No pixel values in margin/padding — uses sx units or Tailwind classes
  • No hardcoded hex colors — uses theme palette or semantic Tailwind tokens
  • No inline style attribute — uses sx or className only
  • No MUI/Tailwind mixing on the same element
  • No inline arrow functions in JSX — handlers defined outside render
  • All 5 API states handled: loading, success, error, empty, disabled
  • Tables/lists render EmptyState when data is empty
  • Icon-only buttons have aria-label. Component works on mobile, tablet, desktop.