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.
src/components, pages, sections, shared layouts, hooks, services, and constants.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/ 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.
UserCard · InvoiceTable · handleSubmitcomponents/ · hooks/ · utils/MAX_RETRY_COUNT · API_BASE_URLIS_, HAS_, CAN_, or SHOULD_.IS_LOADING · HAS_PERMISSIONSpacing & color
Use theme-aware tokens. Never hardcode pixel values or hex colors.
// MUI (1 unit = 8px) sx={{ mt: 2 }} → 16px sx={{ p: 2 }} → 16px // Tailwind (1 unit = 4px) className="mt-4" → 16px className="p-4" → 16px
sx={{ mt: 2 }}className="mt-4"style={{ marginTop: '16px' }}// 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 variant="h5">User List</Typography><h1>User List</h1><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.
sx whenBuilding with MUI primitives — Box, Stack, Grid, Typography, Button, Dialog, Card, TextField.
You need theme-aware colors — color="primary", color="error".
You need responsive breakpoints via Grid xs, sm, md props.
className whenStyling 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.
<Box sx={{ mt: 2, p: 2 }}><div className="mt-4 p-4"><Box className="mt-4" sx={{ p: 2 }}>mixing creates unpredictable CSS specificity
// 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 = 24pxShared 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.
components/. Never duplicate — extend with an optional prop instead.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 container spacing={2}> <Grid item xs={6}> <TextField label="Name" fullWidth {...register('name')} error={!!errors.name} helperText={errors.name?.message} /> </Grid> </Grid>
<LoadingButton loading={isSubmitting} variant="contained">Save</LoadingButton>
<TextField label="Email" /><TextField />Dialogs
All dialogs must follow the standard structure. Never render a dialog without maxWidth and fullWidth.
<Dialog maxWidth="sm" fullWidth> <DialogTitle>Confirm Action</DialogTitle> <DialogContent> {/* content */} </DialogContent> <DialogActions> <Button>Cancel</Button> <Button variant="contained">Confirm</Button> </DialogActions> </Dialog>
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> <CardHeader title="..." /> <CardContent>{/* body */}</CardContent> <CardActions> <Button>Action</Button> </CardActions> </Card>
<Box> · <Stack><div>Buttons & icons
Buttons must be intentional and descriptive. Icons must come from the library — never inline SVG.
variant="contained" with color="primary" for primary actions.<Button>Save Changes</Button><Button>Click</Button><Button startIcon={<AddIcon />}>Add User</Button> <IconButton aria-label="delete"> <DeleteIcon /> </IconButton>
const handleClick = () => {}<Button onClick={handleClick}><Button onClick={() => handleClick(id)}>States & feedback
Every data-fetching component must handle the full lifecycle. Shipping without all five states is incomplete.
if (!data.length) { return <EmptyState /> }
Validation standard
All forms use react-hook-form with Yup schemas. No other validation approach is permitted.
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 label="Email" fullWidth {...register('email')} error={!!errors.email} helperText={errors.email?.message} />
"Name is required""Enter a valid email""Must be at least 6 characters""name_required""Invalid email format""String must be min 6"sections/{RoutePage}/Forms/{FormName}.schema.js ← schema here
sections/{RoutePage}/Forms/{FormName}.jsx ← imports schemaPerformance
Apply React's memoization APIs to prevent unnecessary re-renders. Use only where the cost justifies it.
Responsive rules
Every component must function correctly at all three breakpoints. No fixed widths.
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.
- 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.