diff --git a/src/app/features/call-status/CallControl.tsx b/src/app/features/call-status/CallControl.tsx
index fa5c60823..1f3e929ed 100644
--- a/src/app/features/call-status/CallControl.tsx
+++ b/src/app/features/call-status/CallControl.tsx
@@ -5,6 +5,7 @@ import { StatusDivider } from './components';
import { CallEmbed, useCallControlState } from '../../plugins/call';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { callEmbedAtom } from '../../state/callEmbed';
+import { MobileTouchTarget } from '../../styles/mobile.css';
type MicrophoneButtonProps = {
enabled: boolean;
@@ -31,6 +32,7 @@ function MicrophoneButton({ enabled, onToggle, disabled }: MicrophoneButtonProps
fill="Soft"
radii="300"
size="300"
+ className={MobileTouchTarget}
onClick={toggleMic}
outlined
disabled={disabled || loading}
@@ -66,6 +68,7 @@ function SoundButton({ enabled, onToggle, disabled }: SoundButtonProps) {
fill="Soft"
radii="300"
size="300"
+ className={MobileTouchTarget}
onClick={() => onToggle()}
aria-label={enabled ? 'Deafen' : 'Undeafen'}
aria-pressed={enabled}
@@ -108,6 +111,7 @@ function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
fill="Soft"
radii="300"
size="300"
+ className={MobileTouchTarget}
onClick={toggleVideo}
aria-label={enabled ? 'Stop Video' : 'Start Video'}
aria-pressed={enabled}
@@ -147,6 +151,7 @@ function ScreenShareButton({ enabled, onToggle, disabled }: ScreenShareButtonPro
fill="Soft"
radii="300"
size="300"
+ className={MobileTouchTarget}
onClick={onToggle}
aria-label={enabled ? 'Stop Screenshare' : 'Start Screenshare'}
aria-pressed={enabled}
diff --git a/src/app/features/call/Controls.tsx b/src/app/features/call/Controls.tsx
index 3bf8ac7c2..f5f3e81d5 100644
--- a/src/app/features/call/Controls.tsx
+++ b/src/app/features/call/Controls.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { Icon, IconButton, Icons, Line, Text, Tooltip, TooltipProvider } from 'folds';
import { useAtom } from 'jotai';
import * as css from './styles.css';
+import { MobileTouchTarget } from '../../styles/mobile.css';
import { callChatAtom } from '../../state/callEmbed';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
@@ -36,6 +37,7 @@ export function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) {
fill="Soft"
radii="400"
size="400"
+ className={MobileTouchTarget}
onClick={toggleMic}
aria-label={enabled ? 'Turn Off Microphone' : 'Turn On Microphone'}
outlined
@@ -70,6 +72,7 @@ export function SoundButton({ enabled, onToggle }: SoundButtonProps) {
fill="Soft"
radii="400"
size="400"
+ className={MobileTouchTarget}
onClick={() => onToggle()}
aria-label={enabled ? 'Undeafen' : 'Deafen'}
outlined
@@ -113,6 +116,7 @@ export function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
fill="Soft"
radii="400"
size="400"
+ className={MobileTouchTarget}
onClick={toggleVideo}
outlined
disabled={disabled || loading}
@@ -155,6 +159,7 @@ export function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps)
fill="Soft"
radii="400"
size="400"
+ className={MobileTouchTarget}
onClick={() => onToggle()}
aria-label={enabled ? 'Stop Screenshare' : 'Start Screenshare'}
outlined
@@ -200,6 +205,7 @@ export function FullscreenButton({ isFullscreen, onToggle }: FullscreenButtonPro
fill="Soft"
radii="400"
size="400"
+ className={MobileTouchTarget}
onClick={onToggle}
aria-label={isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
aria-pressed={isFullscreen}
@@ -234,6 +240,7 @@ export function ScreenshareAudioButton({ muted, onToggle }: ScreenshareAudioButt
fill="Soft"
radii="400"
size="400"
+ className={MobileTouchTarget}
onClick={onToggle}
aria-label={muted ? 'Unmute Screenshare Audio' : 'Mute Screenshare Audio'}
aria-pressed={muted}
@@ -266,6 +273,7 @@ export function ChatButton() {
fill="Soft"
radii="400"
size="400"
+ className={MobileTouchTarget}
onClick={() => setChat(!chat)}
aria-label={chat ? 'Close Chat' : 'Open Chat'}
aria-pressed={chat}
diff --git a/src/app/features/room-settings/RoomServerACL.tsx b/src/app/features/room-settings/RoomServerACL.tsx
index 98531dd69..aea24e43d 100644
--- a/src/app/features/room-settings/RoomServerACL.tsx
+++ b/src/app/features/room-settings/RoomServerACL.tsx
@@ -33,6 +33,7 @@ import { SequenceCard } from '../../components/sequence-card';
import { SequenceCardStyle } from '../common-settings/styles.css';
import { stopPropagation } from '../../utils/keyboard';
import { isValidServerPattern, matchesAnyGlob } from '../../utils/serverAcl';
+import { MobileTouchTarget } from '../../styles/mobile.css';
import { useModalStyle } from '../../hooks/useModalStyle';
// ── Types ─────────────────────────────────────────────────────────────────────
@@ -148,6 +149,7 @@ function ServerList({ label, entries, canEdit, onAdd, onRemove }: ServerListProp
size="300"
variant="Background"
radii="300"
+ className={MobileTouchTarget}
aria-label={`Remove ${entry}`}
onClick={() => onRemove(i)}
style={{ flexShrink: 0 }}
diff --git a/src/app/features/room/MembersDrawer.tsx b/src/app/features/room/MembersDrawer.tsx
index 69e6beb33..54fb4ff1b 100644
--- a/src/app/features/room/MembersDrawer.tsx
+++ b/src/app/features/room/MembersDrawer.tsx
@@ -30,6 +30,7 @@ import {
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
import { useVirtualizer } from '@tanstack/react-virtual';
import classNames from 'classnames';
+import { MobileTouchTarget } from '../../styles/mobile.css';
import { Membership } from '../../../types/matrix/room';
import * as css from './MembersDrawer.css';
@@ -460,6 +461,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
variant="Success"
radii="300"
fill="Soft"
+ className={MobileTouchTarget}
onClick={() => mx.invite(room.roomId, knockMember.userId)}
>
Approve
@@ -469,6 +471,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
variant="Critical"
radii="300"
fill="Soft"
+ className={MobileTouchTarget}
onClick={() => mx.kick(room.roomId, knockMember.userId)}
>
Deny
diff --git a/src/app/features/room/thread/ThreadSummary.tsx b/src/app/features/room/thread/ThreadSummary.tsx
index 10bce29e6..875ae5d49 100644
--- a/src/app/features/room/thread/ThreadSummary.tsx
+++ b/src/app/features/room/thread/ThreadSummary.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { Badge, Box, Chip, Icon, Icons, Text, config } from 'folds';
import { MatrixEvent, Room } from 'matrix-js-sdk';
+import { MobileTouchTarget } from '../../../styles/mobile.css';
import { useThreadSummary } from '../../../hooks/useThreadSummary';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
@@ -31,6 +32,7 @@ export function ThreadSummary({ rootEvent, room, onOpen }: ThreadSummaryProps) {
}
after={
unread > 0 ? : undefined
diff --git a/src/app/styles/mobile.css.ts b/src/app/styles/mobile.css.ts
new file mode 100644
index 000000000..e08d9fc65
--- /dev/null
+++ b/src/app/styles/mobile.css.ts
@@ -0,0 +1,18 @@
+import { style } from '@vanilla-extract/css';
+import { toRem } from 'folds';
+
+/**
+ * A 44px minimum touch target on phones for otherwise-small interactive controls
+ * (folds `IconButton`/`Chip` at `size="300"`/`"400"` render ~24–40px, below the
+ * 44px guideline). Apply via `className`; the icon/label stays its normal visual
+ * size — only the hit area grows — and desktop is unchanged (the rule is gated to
+ * `@media (max-width: 750px)`, matching MOBILE_BREAKPOINT).
+ */
+export const MobileTouchTarget = style({
+ '@media': {
+ '(max-width: 750px)': {
+ minWidth: toRem(44),
+ minHeight: toRem(44),
+ },
+ },
+});