Mythic Framework uses React for all user interfaces, rendered inside FiveM’s NUI layer (a Chromium-based browser). Client Lua scripts communicate with React UIs through NUI messages and callbacks.
UI Architecture
Technology Stack
Mythic resources use two UI stacks depending on age:
Legacy UIs React 17 + Redux + ThunkUsed by: HUD, Inventory, MDT, Laptop, most existing resources
Modern UIs React 18 + Zustand + TypeScriptUsed by: New Phone UI (ui-new/), newer resources
Common across both:
Material-UI v5 — Component library
Emotion — CSS-in-JS styling
Webpack 5 — Module bundler
UI Resources
mythic-hud Health, armor, status bars, vehicle info, minimap, notifications
mythic-phone Smartphone with contacts, messages, calls, email, camera, apps
mythic-inventory Drag-and-drop inventory, tooltips, crafting, shops
mythic-mdt Police MDT — warrants, person/vehicle lookup, dispatch, reports
mythic-laptop Laptop interface with email, documents, browser
mythic-characters Character creation, appearance customization, selection
mythic-menu Context menus with nested submenus and keyboard navigation
mythic-chat In-game chat with commands
Client-UI Communication
Lua to React (SendNUIMessage)
-- client/main.lua
-- Open UI with data
SendNUIMessage ({
type = 'OPEN_INVENTORY' ,
data = {
inventory = inventoryData ,
maxSlots = 50 ,
maxWeight = 100
}
})
-- Update specific data
SendNUIMessage ({
type = 'UPDATE_MONEY' ,
data = { cash = 5000 , bank = 25000 }
})
-- Close UI
SendNUIMessage ({ type = 'CLOSE_INVENTORY' })
React side — listening for messages:
// Legacy (Redux pattern)
useEffect (() => {
const handleMessage = ( event ) => {
const { type , data } = event . data ;
switch ( type ) {
case 'OPEN_INVENTORY' :
setInventory ( data . inventory );
setVisible ( true );
break ;
case 'CLOSE_INVENTORY' :
setVisible ( false );
break ;
}
};
window . addEventListener ( 'message' , handleMessage );
return () => window . removeEventListener ( 'message' , handleMessage );
}, []);
React to Lua (NUI Callbacks)
// ui/src/utils/fetchNui.js
export const fetchNui = async ( eventName , data = {}) => {
const resourceName = window . GetParentResourceName
? window . GetParentResourceName ()
: 'mythic-inventory' ;
const response = await fetch ( `https:// ${ resourceName } / ${ eventName } ` , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( data )
});
return await response . json ();
};
// Usage in a component
const handleUseItem = ( slot ) => {
fetchNui ( 'useItem' , { slot }). then (( response ) => {
if ( response . success ) {
console . log ( 'Item used' );
}
});
};
Lua side — handling NUI callbacks:
-- client/ui.lua
RegisterNUICallback ( 'useItem' , function ( data , cb )
local slot = data . slot
TriggerServerEvent ( 'mythic-inventory:server:UseItem' , slot )
cb ({ success = true })
end )
RegisterNUICallback ( 'closeInventory' , function ( data , cb )
SetNuiFocus ( false , false )
cb ( 'ok' )
end )
Communication Flow
Custom Hooks
useNuiEvent
Simplify NUI message handling:
import { useEffect } from 'react' ;
export const useNuiEvent = ( action , handler ) => {
useEffect (() => {
const handleMessage = ( event ) => {
const { type , data } = event . data ;
if ( type === action ) {
handler ( data );
}
};
window . addEventListener ( 'message' , handleMessage );
return () => window . removeEventListener ( 'message' , handleMessage );
}, [ action , handler ]);
};
// Usage
function InventoryApp () {
const [ visible , setVisible ] = useState ( false );
useNuiEvent ( 'OPEN_INVENTORY' , () => setVisible ( true ));
useNuiEvent ( 'CLOSE_INVENTORY' , () => setVisible ( false ));
}
State Management
Redux (Legacy UIs)
// Store setup
import { createStore , applyMiddleware } from 'redux' ;
import thunk from 'redux-thunk' ;
const store = createStore ( rootReducer , applyMiddleware ( thunk ));
// Reducer
const initialState = { items: [], visible: false };
export default function inventoryReducer ( state = initialState , action ) {
switch ( action . type ) {
case 'SET_INVENTORY' :
return { ... state , items: action . payload . items };
case 'TOGGLE_VISIBLE' :
return { ... state , visible: ! state . visible };
default :
return state ;
}
}
// Component
function Inventory () {
const items = useSelector ( state => state . inventory . items );
const dispatch = useDispatch ();
const handleUseItem = ( slot ) => dispatch ( useItem ( slot ));
return (
< div className = "inventory" >
{ items . map (( item , i ) => (
< ItemSlot key = { i } item = { item } onClick = { () => handleUseItem ( i ) } />
)) }
</ div >
);
}
Zustand (Modern UIs)
// Store definition
import { create } from 'zustand' ;
interface PhoneState {
visible : boolean ;
notifications : Notification [];
setVisible : ( v : boolean ) => void ;
addNotification : ( n : Notification ) => void ;
}
export const usePhoneStore = create < PhoneState >(( set ) => ({
visible: false ,
notifications: [],
setVisible : ( visible ) => set ({ visible }),
addNotification : ( n ) => set (( s ) => ({
notifications: [ ... s . notifications , n ]
})),
}));
// Component usage
function PhoneApp () {
const visible = usePhoneStore (( s ) => s . visible );
const notifications = usePhoneStore (( s ) => s . notifications );
if ( ! visible ) return null ;
return < div > ... </ div > ;
}
Best Practices
Always manage NUI focus properly in Lua: -- Open UI: enable cursor and keyboard input
SendNUIMessage ({ type = 'OPEN_UI' })
SetNuiFocus ( true , true )
-- Close UI: disable cursor and input
RegisterNUICallback ( 'close' , function ( data , cb )
SetNuiFocus ( false , false )
cb ( 'ok' )
end )
Always handle ESC to close UI: useEffect (() => {
const handleEscape = ( e ) => {
if ( e . key === 'Escape' && visible ) {
fetchNui ( 'close' );
setVisible ( false );
}
};
window . addEventListener ( 'keydown' , handleEscape );
return () => window . removeEventListener ( 'keydown' , handleEscape );
}, [ visible ]);
Don’t render hidden UIs — return null: function MyUI () {
const [ visible , setVisible ] = useState ( false );
useNuiEvent ( 'OPEN_UI' , () => setVisible ( true ));
useNuiEvent ( 'CLOSE_UI' , () => setVisible ( false ));
if ( ! visible ) return null ;
return < div > ... </ div > ;
}
Building UIs
# Install dependencies
npm install
# Development mode with hot reload
npm run dev
# Production build (outputs to dist/)
npm run build
# Watch mode (rebuilds on file changes)
npm run watch
Built UI files go into dist/ which is referenced by ui_page in fxmanifest.lua. Always run npm run build before testing in-game.
Next Steps
Resource Structure How UI fits into resource organization
Component System Server/client components that power UIs
Event System Events that trigger UI updates
HUD API HUD component API reference