diff --git a/public/index.html b/public/index.html
index edc9d8e1..59487592 100644
--- a/public/index.html
+++ b/public/index.html
@@ -13,29 +13,10 @@
       #app-root { display:flex; height:100%; }
 
       /* Remove focus state on inputs */
-      input:focus {
+      input:focus,
+      textarea:focus {
         outline: 0;
       }
-
-      /* These styles are for src/view/com/modals/WebModal */
-      div[data-modal-overlay] {
-        position: fixed;
-        top: 0;
-        left: 0;
-        background: #0004;
-        width: 100vw;
-        height: 100vh;
-      }
-      div[data-modal-container] {
-        position: fixed;
-        top: 20vh;
-        left: calc(50vw - 300px);
-        width: 600px;
-        padding: 20px;
-        background: #fff;
-        border-radius: 10px;
-        box-shadow: 0 5px 10px #0005;
-      }
     </style>
   </head>
   <body>
diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx
index 64e75328..2f30a1cf 100644
--- a/src/view/com/composer/ComposePost.tsx
+++ b/src/view/com/composer/ComposePost.tsx
@@ -11,25 +11,19 @@ import {
   TouchableWithoutFeedback,
   View,
 } from 'react-native'
-import PasteInput, {
-  PastedFile,
-  PasteInputRef,
-} from '@mattermost/react-native-paste-input'
 import LinearGradient from 'react-native-linear-gradient'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
-import {useAnalytics} from '@segment/analytics-react-native'
+// import {useAnalytics} from '@segment/analytics-react-native' TODO
 import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view'
 import {Autocomplete} from './Autocomplete'
 import {ExternalEmbed} from './ExternalEmbed'
 import {Text} from '../util/text/Text'
 import * as Toast from '../util/Toast'
-// @ts-ignore no type definition -prf
-import ProgressCircle from 'react-native-progress/Circle'
-// @ts-ignore no type definition -prf
-import ProgressPie from 'react-native-progress/Pie'
+import {TextInput, TextInputRef} from './text-input/TextInput'
+import {CharProgress} from './char-progress/CharProgress'
 import {TextLink} from '../util/Link'
 import {UserAvatar} from '../util/UserAvatar'
 import {useStores} from '../../../state'
@@ -49,7 +43,6 @@ import {SelectedPhoto} from './SelectedPhoto'
 import {usePalette} from '../../lib/hooks/usePalette'
 
 const MAX_TEXT_LENGTH = 256
-const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH
 const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
 
 export const ComposePost = observer(function ComposePost({
@@ -63,10 +56,10 @@ export const ComposePost = observer(function ComposePost({
   onPost?: ComposerOpts['onPost']
   onClose: () => void
 }) {
-  const {track} = useAnalytics()
+  // const {track} = useAnalytics() TODO
   const pal = usePalette('default')
   const store = useStores()
-  const textInput = useRef<PasteInputRef>(null)
+  const textInput = useRef<TextInputRef>(null)
   const [isProcessing, setIsProcessing] = useState(false)
   const [processingState, setProcessingState] = useState('')
   const [error, setError] = useState('')
@@ -80,7 +73,6 @@ export const ComposePost = observer(function ComposePost({
   )
   const [selectedPhotos, setSelectedPhotos] = useState<string[]>([])
 
-  // Using default import (React.use...) instead of named import (use...) to be able to mock store's data in jest environment
   const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
     () => new UserAutocompleteViewModel(store),
     [store],
@@ -219,19 +211,18 @@ export const ComposePost = observer(function ComposePost({
       }
     }
   }
-  const onPaste = async (err: string | undefined, files: PastedFile[]) => {
+  const onPaste = async (err: string | undefined, uris: string[]) => {
     if (err) {
       return setError(cleanError(err))
     }
     if (selectedPhotos.length >= 4) {
       return
     }
-    const imgFile = files.find(file => /\.(jpe?g|png)$/.test(file.fileName))
-    if (!imgFile) {
-      return
+    const imgUri = uris.find(uri => /\.(jpe?g|png)$/.test(uri))
+    if (imgUri) {
+      const finalImgPath = await cropPhoto(imgUri)
+      onSelectPhotos([...selectedPhotos, finalImgPath])
     }
-    const finalImgPath = await cropPhoto(imgFile.uri)
-    onSelectPhotos([...selectedPhotos, finalImgPath])
   }
   const onPressCancel = () => hackfixOnClose()
   const onPressPublish = async () => {
@@ -257,9 +248,10 @@ export const ComposePost = observer(function ComposePost({
         autocompleteView.knownHandles,
         setProcessingState,
       )
-      track('Create Post', {
-        imageCount: selectedPhotos.length,
-      })
+      // TODO
+      // track('Create Post', {
+      //   imageCount: selectedPhotos.length,
+      // })
     } catch (e: any) {
       setError(cleanError(e.message))
       setIsProcessing(false)
@@ -276,7 +268,6 @@ export const ComposePost = observer(function ComposePost({
   }
 
   const canPost = text.length <= MAX_TEXT_LENGTH
-  const progressColor = text.length > DANGER_TEXT_LENGTH ? '#e60000' : undefined
 
   const selectTextInputLayout =
     selectedPhotos.length !== 0
@@ -311,7 +302,7 @@ export const ComposePost = observer(function ComposePost({
     <KeyboardAvoidingView
       testID="composePostView"
       behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
-      style={[pal.view, styles.outer]}>
+      style={styles.outer}>
       <TouchableWithoutFeedback onPressIn={onPressContainer}>
         <SafeAreaView style={s.flex1}>
           <View style={styles.topbar}>
@@ -396,22 +387,19 @@ export const ComposePost = observer(function ComposePost({
                 avatar={store.me.avatar}
                 size={50}
               />
-              <PasteInput
+              <TextInput
                 testID="composerTextInput"
-                ref={textInput}
-                multiline
-                scrollEnabled
+                innerRef={textInput}
                 onChangeText={(str: string) => onChangeText(str)}
                 onPaste={onPaste}
                 placeholder={selectTextInputPlaceholder}
-                placeholderTextColor={pal.colors.textLight}
                 style={[
                   pal.text,
                   styles.textInput,
                   styles.textInputFormatting,
                 ]}>
                 {textDecorated}
-              </PasteInput>
+              </TextInput>
             </View>
             <SelectedPhoto
               selectedPhotos={selectedPhotos}
@@ -450,31 +438,7 @@ export const ComposePost = observer(function ComposePost({
               />
             </TouchableOpacity>
             <View style={s.flex1} />
-            <Text style={[s.mr10, {color: progressColor}]}>
-              {MAX_TEXT_LENGTH - text.length}
-            </Text>
-            <View>
-              {text.length > DANGER_TEXT_LENGTH ? (
-                <ProgressPie
-                  size={30}
-                  borderWidth={4}
-                  borderColor={progressColor}
-                  color={progressColor}
-                  progress={Math.min(
-                    (text.length - MAX_TEXT_LENGTH) / MAX_TEXT_LENGTH,
-                    1,
-                  )}
-                />
-              ) : (
-                <ProgressCircle
-                  size={30}
-                  borderWidth={1}
-                  borderColor={colors.gray2}
-                  color={progressColor}
-                  progress={text.length / MAX_TEXT_LENGTH}
-                />
-              )}
-            </View>
+            <CharProgress count={text.length} />
           </View>
           <Autocomplete
             active={autocompleteView.isActive}
@@ -504,7 +468,6 @@ const styles = StyleSheet.create({
     flexDirection: 'column',
     flex: 1,
     padding: 15,
-    paddingBottom: Platform.OS === 'ios' ? 0 : 50,
     height: '100%',
   },
   topbar: {
diff --git a/src/view/com/composer/char-progress/CharProgress.tsx b/src/view/com/composer/char-progress/CharProgress.tsx
new file mode 100644
index 00000000..d4093064
--- /dev/null
+++ b/src/view/com/composer/char-progress/CharProgress.tsx
@@ -0,0 +1,41 @@
+import React from 'react'
+import {View} from 'react-native'
+import {Text} from '../../util/text/Text'
+// @ts-ignore no type definition -prf
+import ProgressCircle from 'react-native-progress/Circle'
+// @ts-ignore no type definition -prf
+import ProgressPie from 'react-native-progress/Pie'
+import {s, colors} from '../../../lib/styles'
+
+const MAX_TEXT_LENGTH = 256
+const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH
+
+export function CharProgress({count}: {count: number}) {
+  const progressColor = count > DANGER_TEXT_LENGTH ? '#e60000' : undefined
+  return (
+    <>
+      <Text style={[s.mr10, {color: progressColor}]}>
+        {MAX_TEXT_LENGTH - count}
+      </Text>
+      <View>
+        {count > DANGER_TEXT_LENGTH ? (
+          <ProgressPie
+            size={30}
+            borderWidth={4}
+            borderColor={progressColor}
+            color={progressColor}
+            progress={Math.min((count - MAX_TEXT_LENGTH) / MAX_TEXT_LENGTH, 1)}
+          />
+        ) : (
+          <ProgressCircle
+            size={30}
+            borderWidth={1}
+            borderColor={colors.gray2}
+            color={progressColor}
+            progress={count / MAX_TEXT_LENGTH}
+          />
+        )}
+      </View>
+    </>
+  )
+}
diff --git a/src/view/com/composer/char-progress/CharProgress.web.tsx b/src/view/com/composer/char-progress/CharProgress.web.tsx
new file mode 100644
index 00000000..6bdcc139
--- /dev/null
+++ b/src/view/com/composer/char-progress/CharProgress.web.tsx
@@ -0,0 +1,39 @@
+import React from 'react'
+import {View} from 'react-native'
+import {Text} from '../util/text/Text'
+import {s} from '../../lib/styles'
+
+const MAX_TEXT_LENGTH = 256
+const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH
+
+export function CharProgress({count}: {count: number}) {
+  const progressColor = count > DANGER_TEXT_LENGTH ? '#e60000' : undefined
+  return (
+    <>
+      <Text style={[s.mr10, {color: progressColor}]}>
+        {MAX_TEXT_LENGTH - count}
+      </Text>
+      <View>
+        {
+          null /* TODO count > DANGER_TEXT_LENGTH ? (
+          <ProgressPie
+            size={30}
+            borderWidth={4}
+            borderColor={progressColor}
+            color={progressColor}
+            progress={Math.min((count - MAX_TEXT_LENGTH) / MAX_TEXT_LENGTH, 1)}
+          />
+        ) : (
+          <ProgressCircle
+            size={30}
+            borderWidth={1}
+            borderColor={colors.gray2}
+            color={progressColor}
+            progress={count / MAX_TEXT_LENGTH}
+          />
+        )*/
+        }
+      </View>
+    </>
+  )
+}
diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx
new file mode 100644
index 00000000..3c5dacf8
--- /dev/null
+++ b/src/view/com/composer/text-input/TextInput.tsx
@@ -0,0 +1,54 @@
+import React from 'react'
+import {StyleProp, TextStyle} from 'react-native'
+import PasteInput, {
+  PastedFile,
+  PasteInputRef,
+} from '@mattermost/react-native-paste-input'
+import {usePalette} from '../../../lib/hooks/usePalette'
+
+export type TextInputRef = PasteInputRef
+
+interface TextInputProps {
+  testID: string
+  innerRef: React.Ref<TextInputRef>
+  placeholder: string
+  style: StyleProp<TextStyle>
+  onChangeText: (str: string) => void
+  onPaste: (err: string | undefined, uris: string[]) => void
+}
+
+export function TextInput({
+  testID,
+  innerRef,
+  placeholder,
+  style,
+  onChangeText,
+  onPaste,
+  children,
+}: React.PropsWithChildren<TextInputProps>) {
+  const pal = usePalette('default')
+  const onPasteInner = (err: string | undefined, files: PastedFile[]) => {
+    if (err) {
+      onPaste(err, [])
+    } else {
+      onPaste(
+        undefined,
+        files.map(f => f.uri),
+      )
+    }
+  }
+  return (
+    <PasteInput
+      testID={testID}
+      ref={innerRef}
+      multiline
+      scrollEnabled
+      onChangeText={(str: string) => onChangeText(str)}
+      onPaste={onPasteInner}
+      placeholder={placeholder}
+      placeholderTextColor={pal.colors.textLight}
+      style={style}>
+      {children}
+    </PasteInput>
+  )
+}
diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
new file mode 100644
index 00000000..6960bf7a
--- /dev/null
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -0,0 +1,51 @@
+import React from 'react'
+import {
+  StyleProp,
+  StyleSheet,
+  TextInput as RNTextInput,
+  TextStyle,
+} from 'react-native'
+import {usePalette} from '../../lib/hooks/usePalette'
+import {addStyle} from '../../lib/addStyle'
+
+export type TextInputRef = RNTextInput
+
+interface TextInputProps {
+  testID: string
+  innerRef: React.Ref<TextInputRef>
+  placeholder: string
+  style: StyleProp<TextStyle>
+  onChangeText: (str: string) => void
+  onPaste: (err: string | undefined, uris: string[]) => void
+}
+
+export function TextInput({
+  testID,
+  innerRef,
+  placeholder,
+  style,
+  onChangeText,
+  children,
+}: React.PropsWithChildren<TextInputProps>) {
+  const pal = usePalette('default')
+  style = addStyle(style, styles.input)
+  return (
+    <RNTextInput
+      testID={testID}
+      ref={innerRef}
+      multiline
+      scrollEnabled
+      onChangeText={(str: string) => onChangeText(str)}
+      placeholder={placeholder}
+      placeholderTextColor={pal.colors.textLight}
+      style={style}>
+      {children}
+    </RNTextInput>
+  )
+}
+
+const styles = StyleSheet.create({
+  input: {
+    minHeight: 140,
+  },
+})
diff --git a/src/view/shell/mobile/Composer.tsx b/src/view/shell/mobile/Composer.tsx
index a19a4704..c93931ab 100644
--- a/src/view/shell/mobile/Composer.tsx
+++ b/src/view/shell/mobile/Composer.tsx
@@ -48,9 +48,6 @@ export const Composer = observer(
       ],
     }
 
-    // events
-    // =
-
     // rendering
     // =
 
diff --git a/src/view/shell/web/Composer.tsx b/src/view/shell/web/Composer.tsx
new file mode 100644
index 00000000..63904009
--- /dev/null
+++ b/src/view/shell/web/Composer.tsx
@@ -0,0 +1,65 @@
+import React from 'react'
+import {observer} from 'mobx-react-lite'
+import {StyleSheet, View} from 'react-native'
+import {ComposePost} from '../../com/composer/ComposePost'
+import {ComposerOpts} from '../../../state/models/shell-ui'
+import {usePalette} from '../../lib/hooks/usePalette'
+
+export const Composer = observer(
+  ({
+    active,
+    replyTo,
+    imagesOpen,
+    onPost,
+    onClose,
+  }: {
+    active: boolean
+    winHeight: number
+    replyTo?: ComposerOpts['replyTo']
+    imagesOpen?: ComposerOpts['imagesOpen']
+    onPost?: ComposerOpts['onPost']
+    onClose: () => void
+  }) => {
+    const pal = usePalette('default')
+
+    // rendering
+    // =
+
+    if (!active) {
+      return <View />
+    }
+
+    return (
+      <View style={styles.mask}>
+        <View style={[styles.container, pal.view]}>
+          <ComposePost
+            replyTo={replyTo}
+            imagesOpen={imagesOpen}
+            onPost={onPost}
+            onClose={onClose}
+          />
+        </View>
+      </View>
+    )
+  },
+)
+
+const styles = StyleSheet.create({
+  mask: {
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    width: '100%',
+    height: '100%',
+    backgroundColor: '#000c',
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  container: {
+    maxWidth: 600,
+    width: '100%',
+    paddingVertical: 0,
+    paddingHorizontal: 2,
+    borderRadius: 8,
+  },
+})
diff --git a/src/view/shell/web/left-column.tsx b/src/view/shell/web/DesktopLeftColumn.tsx
similarity index 100%
rename from src/view/shell/web/left-column.tsx
rename to src/view/shell/web/DesktopLeftColumn.tsx
diff --git a/src/view/shell/web/right-column.tsx b/src/view/shell/web/DesktopRightColumn.tsx
similarity index 100%
rename from src/view/shell/web/right-column.tsx
rename to src/view/shell/web/DesktopRightColumn.tsx
diff --git a/src/view/shell/web/index.tsx b/src/view/shell/web/index.tsx
index a4232eab..0eb5cf75 100644
--- a/src/view/shell/web/index.tsx
+++ b/src/view/shell/web/index.tsx
@@ -3,13 +3,14 @@ import {observer} from 'mobx-react-lite'
 import {View, StyleSheet} from 'react-native'
 import {useStores} from '../../../state'
 import {match, MatchResult} from '../../routes'
-import {DesktopLeftColumn} from './left-column'
-import {DesktopRightColumn} from './right-column'
+import {DesktopLeftColumn} from './DesktopLeftColumn'
+import {DesktopRightColumn} from './DesktopRightColumn'
 import {Onboard} from '../../screens/Onboard'
 import {Login} from '../../screens/Login'
 import {ErrorBoundary} from '../../com/util/ErrorBoundary'
 import {Lightbox} from '../../com/lightbox/Lightbox'
 import {Modal} from '../../com/modals/Modal'
+import {Composer} from './Composer'
 import {usePalette} from '../../lib/hooks/usePalette'
 import {s} from '../../lib/styles'
 
@@ -49,6 +50,14 @@ export const WebShell: React.FC = observer(() => {
       ))}
       <DesktopLeftColumn />
       <DesktopRightColumn />
+      <Composer
+        active={store.shell.isComposerActive}
+        onClose={() => store.shell.closeComposer()}
+        winHeight={0}
+        replyTo={store.shell.composerOpts?.replyTo}
+        imagesOpen={store.shell.composerOpts?.imagesOpen}
+        onPost={store.shell.composerOpts?.onPost}
+      />
       <Modal />
       <Lightbox />
     </View>