Themes, backgrounds, slots, and headless UI for Alaznah Calling.
Customization
Restyle the built-in call UI, swap pieces with slots, replace whole screens, or go headless with hooks only.
Base integration paths: Call UI · component list: UI Components.
Imports
import { CallingUI, useCallingClient, useCall, useIncomingCall } from '@alaznah/calling';
import {
defaultCallingTheme,
mergeTheme,
type CallingTheme,
} from '@alaznah/calling/ui';CallingUI is on @alaznah/calling. Theme helpers and lower-level screens live on @alaznah/calling/ui.
1. Theme
CallingUI accepts theme?: Partial<CallingTheme>. Internally it merges with defaults.
import { CallingUI, useCallingClient } from '@alaznah/calling';
import { mergeTheme } from '@alaznah/calling/ui';
const theme = mergeTheme({
colors: {
accent: '#2563eb',
danger: '#e83829',
background: '#0b1220',
text: '#f8fafc',
textMuted: '#94a3b8',
},
});
function Overlay() {
const client = useCallingClient();
return <CallingUI client={client} theme={theme} />;
}Or pass a partial object directly:
<CallingUI
client={client}
theme={{
colors: { accent: '#00a884', background: '#0b141a' },
}}
/>CallingTheme tokens
| Group | Keys (typical) |
|---|---|
colors | background, surface, text, textMuted, accent, danger, success, control, controlActive, controlBar, iconDisabled, overlay |
spacing | xs, sm, md, lg, xl |
typography | title, subtitle, body, caption (font sizes) |
radii | control, avatar, card |
icons | control (icon size) |
Start from defaultCallingTheme (exported) and override only what you need via mergeTheme({ ... }).
import { defaultCallingTheme, mergeTheme } from '@alaznah/calling/ui';
const theme = mergeTheme({
colors: { ...defaultCallingTheme.colors, accent: '#7c3aed' },
typography: { title: 26 },
});2. Backgrounds
Brand the ringing / active surfaces:
<CallingUI
client={client}
backgroundColor="#0b1220"
backgroundImage={require('./assets/call-bg.png')}
/>| Prop | Type | Purpose |
|---|---|---|
backgroundColor | string | Solid fill behind call UI |
backgroundImage | ImageSourcePropType | Optional image backdrop |
3. Slots (partial UI swap)
Override pieces without rewriting the whole tree. Pass slots into CallingUI (forwarded to incoming / active screens):
<CallingUI
client={client}
slots={{
renderAvatar: (call) => <MyAvatar userId={call.peerId} />,
renderHeader: (call) => <Text>{call.peerId}</Text>,
renderStatus: (call) => <Text>{call.state}</Text>,
renderControls: (call) => (
<MyControls call={call} client={client} />
),
renderOverlay: (call) => <MyBanner call={call} />,
}}
/>| Slot | Purpose |
|---|---|
renderAvatar | Peer avatar |
renderHeader | Title / name area |
renderStatus | Subtitle / state / timer line |
renderControls | Replace default control dock |
renderOverlay | Extra layer on top |
Slot APIs also appear on Props.
4. Replace whole screens
<CallingUI
client={client}
renderIncomingScreen={({ call, onAccept, onReject }) => (
<MyIncoming call={call} onAccept={onAccept} onReject={onReject} />
)}
renderActiveCallScreen={({ call, onEnd }) => (
<MyActive call={call} onEnd={onEnd} />
)}
/>Keep accept/reject/end wired through these callbacks (or call client.accept / reject / end yourself) so signaling stays correct.
Accept UX reminder: default built-in screen uses swipe up to accept — if you build custom incoming, define your own gesture but still call onAccept / client.accept.
5. Headless (no CallingUI)
function MyCallHost() {
const client = useCallingClient();
const call = useCall();
const incoming = useIncomingCall();
if (incoming) {
return (
<MyIncoming
call={incoming}
onAccept={() => void client.accept(incoming.callId)}
onReject={() => void client.reject(incoming.callId, 'declined')}
/>
);
}
if (call) {
return (
<MyActive
call={call}
muted={call.muted}
onMute={(m) => void client.setMuted(m)}
onSpeaker={(s) => void client.setSpeaker(s)}
onEnd={() => void client.end(call.callId)}
/>
);
}
return null;
}Video in a custom layout:
import { LocalVideoView, RemoteVideoView } from '@alaznah/calling';
<RemoteVideoView stream={call.remoteStream} objectFit="cover" />
<LocalVideoView stream={call.localStream} mirror objectFit="cover" />6. Keep native & JS incoming in sync
If you show your own (or Alaznah) in-app incoming UI, suppress duplicate system UI and drain native actions on resume:
useEffect(() => {
client.setNativeIncomingSuppressed(true);
return () => client.setNativeIncomingSuppressed(false);
}, [client]);
// On AppState → active:
await client.drainNativeIncomingAction();
await client.syncPendingCalls();Full flow: Incoming Calls.
Quick recipes
Brand colors only
<CallingUI
client={client}
theme={{ colors: { accent: '#128C7E', background: '#075E54' } }}
/>Custom header, keep controls
<CallingUI
client={client}
slots={{
renderHeader: (call) => <Text style={{ color: '#fff' }}>{call.peerId}</Text>,
}}
/>Fully custom incoming, default active
<CallingUI
client={client}
renderIncomingScreen={({ call, onAccept, onReject }) => (
<MyIncoming call={call} onAccept={onAccept} onReject={onReject} />
)}
/>Related
- Call UI — which path to pick
- UI Components · Props
- Incoming Calls · Hooks