Alaznah

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

ts
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.

tsx
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:

tsx
<CallingUI
  client={client}
  theme={{
    colors: { accent: '#00a884', background: '#0b141a' },
  }}
/>

CallingTheme tokens

GroupKeys (typical)
colorsbackground, surface, text, textMuted, accent, danger, success, control, controlActive, controlBar, iconDisabled, overlay
spacingxs, sm, md, lg, xl
typographytitle, subtitle, body, caption (font sizes)
radiicontrol, avatar, card
iconscontrol (icon size)

Start from defaultCallingTheme (exported) and override only what you need via mergeTheme({ ... }).

ts
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:

tsx
<CallingUI
  client={client}
  backgroundColor="#0b1220"
  backgroundImage={require('./assets/call-bg.png')}
/>
PropTypePurpose
backgroundColorstringSolid fill behind call UI
backgroundImageImageSourcePropTypeOptional image backdrop

3. Slots (partial UI swap)

Override pieces without rewriting the whole tree. Pass slots into CallingUI (forwarded to incoming / active screens):

tsx
<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} />,
  }}
/>
SlotPurpose
renderAvatarPeer avatar
renderHeaderTitle / name area
renderStatusSubtitle / state / timer line
renderControlsReplace default control dock
renderOverlayExtra layer on top

Slot APIs also appear on Props.


4. Replace whole screens

tsx
<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)

tsx
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:

tsx
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:

ts
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

tsx
<CallingUI
  client={client}
  theme={{ colors: { accent: '#128C7E', background: '#075E54' } }}
/>

Custom header, keep controls

tsx
<CallingUI
  client={client}
  slots={{
    renderHeader: (call) => <Text style={{ color: '#fff' }}>{call.peerId}</Text>,
  }}
/>

Fully custom incoming, default active

tsx
<CallingUI
  client={client}
  renderIncomingScreen={({ call, onAccept, onReject }) => (
    <MyIncoming call={call} onAccept={onAccept} onReject={onReject} />
  )}
/>