Notes on learning stage of developing my own React Native education app using Expo Framework - Part 5

Last updated on 20 Oct. 2025

Main tutorial used for learning: Simon Grimm: Build Your Own MASTERCLASS clone in React Native, https://www.youtube.com/watch?v=fO3D8lNs10c , 4 hrs. 42 mins, Mar. 2025.

Jump to Details section (an easy way to get to just after end of main notes section and then scroll up to see latest main notes in this post).

=======================================

13:30, 18 October 2025

To do (from older notes):
  • After above, production deploy can be considered. With prod. deploy change below env var in .env.cloud.fs:
    • # EXPO_PUBLIC_WEB_SOON_URL=http://10.50.171.151:8081/soon
To do (new):
  • Done by dropping window width change event altogether: Debounce window width change event handling (comment out console.log)
  • In Browse courses -> particular course -> course page  and in Course overview page:
    • The image at the top gets vertically clipped by a large amount when window width is large like on PC desktop full window width case. It is not a big issue but if the same solution to above issue which was applied in home page can be easily applied here, it may be worth trying it out.
  • Done: In Mobile web view: In Portrait mode, Browse Courses and My Courses screen have course cards which are horizontally clipped. In landscape mode, this clipping issue is not there.
  • In Mobile Android app: Browse Courses screen extends into status bar. [But Course Overview screen does not do so.]
  • In Mobile Android app, Browse Courses screen can be dragged down to close the screen. That seems to be the only screen which has this feature. Is it something SG used on this screen alone to demonstrate the feature? In web app, Browse Courses screen cannot be dragged (mouse click-and-drag) in any direction.
  • Done: In Mobile web app, lesson screen in landscape mode shows drawer in open state on the left which takes up lot of space. The video box is very small. In Portrait view, video box is quite Ok and is only slightly less than in mobile android app, and the drawer is in collapsed state.
    • Even in desktop web app, I think the open drawer size can be reduced but that I think should be considered only as a last resort.
------- end To Do list ---------------

Commit: Commented out not needed console.log statements across project

Commit: Improved Home screen: used max-width and dropped event handling code; Also use max width of 800px

================

[For HomeBlock.tsx: About Home screen doing elastic bounce on Android but not on web.]

Me: Oh I see! It is ScrollView on Native that does the bounce but which does not do the bounce on web.

ChatGPT (CG) said: Exactly ✅ — you’ve got it 100% right!

To restate in simple terms:

  • On Android and iOS, React Native’s <ScrollView> uses the native scrolling physics of each platform.

    • These include that “elastic overscroll bounce” when you pull down or up past the edges of the content.

    • That bounce is built into the native OS scrolling behavior, not your animation code.

  • On web, the same <ScrollView> is rendered using a regular HTML <div> with overflow scrolling — browsers typically don’t do that elastic bounce (except maybe on macOS Safari with trackpad “rubber-band” effect).

So:
🌀 The bounce = normal native ScrollView behavior.
🖥️ No bounce on web = normal web scroll behavior.
🎨 Your Animated.View fade-in only affects how the block appears — not scrolling physics.

If you ever want a uniform, no-bounce behavior across all platforms, just add:

bounces={false}
overScrollMode="never"

to your <ScrollView> — and the screen will feel firm on Android/iOS just like it already does on web.

================

CG (summary): 

useWindowDimensions() is a React Native hook that automatically provides the current screen width and height, and reactively updates whenever the screen size or orientation changes. Unlike Dimensions.get('window'), which only fetches the initial values and requires manual event listeners to handle updates, useWindowDimensions() is fully managed by React Native’s internal system. This means you don’t need to add resize listeners or debounce events — the hook efficiently updates your component only when necessary.
👉 In short: useWindowDimensions() is a smart, auto-updating, and debounced alternative to Dimensions.get(), perfect for responsive layouts with minimal code.

...

Note: Responsive Layouts in React Native — Dimensions.get() vs useWindowDimensions()

In early versions of our HomeBlock.tsx, we used Dimensions.get('window') along with a manual event listener to track window size changes and adjust image heights and layout. This gave us fine-grained control for experimentation but required extra code for subscribing, updating state, and debouncing events to avoid excessive re-renders.

React Native provides a simpler alternative: useWindowDimensions(). This hook automatically returns the current screen width and height and reactively updates whenever the screen size or orientation changes, without needing any event listeners or debouncing. It works on both mobile and web, making it ideal for responsive layouts.

In short: useWindowDimensions() is a smart, auto-updating, and debounced replacement for Dimensions.get(), perfect for clean and responsive UI code.


...

Lesson learned: When building responsive layouts in React Native, start with useWindowDimensions() for automatic, clean, and reactive screen-size handling — reserve Dimensions.get() with listeners only if you need very fine-grained, low-level control.

==================

We eventually could entirely drop window size related code in  HomeBlock.tsx. So the above note is a learning note for future.

Commit: Mobile web portrait mode: Fixed horizontal clipping of course cards in Browse Courses and My Courses screens
....

Me: In the following file: app\(app)\(authenticated)\course\[slug]\[lesson]\_layout.tsx I want to keep the drawerType as 'front' for special case of mobile web browser in landscape mode even if dimensions.width is > 768

[Long exchange on how to detect mobile web landscape mode. app\(app)\(authenticated)\course\[slug]\[lesson]\_layout-mob-lscape.tsx.txt has some of the attempts but they had some issue or the other. I have given below some key points from the exchange.]

// Detect mobile web
  const isMobileWeb =
    Platform.OS === 'web' &&
    (navigator.userAgent.match(/Android/i) ||
      navigator.userAgent.match(/iPhone|iPad|iPod/i));

Is that a good way to detect mobile web? Will all mobile browsers on Android or iOS provide correct userAgent info matching above code?
...

how about this approach:
  const { width, height } = useWindowDimensions();

  const isLandscape = width > height;
...

Even on PC, for large window width, it gets viewed as landscape.
So that test for mobile landscape is not good enough
...

In short:
  • Aspect ratio alone is insufficient.

  • Combine touch detection + orientation to detect mobile landscape reliably.

...
you can have a tablet instead of pc. So that too won't work.
...

I think it may be best to use user-agent as we do not have any other clear way
...

Just struck me that user agent may be Android or iOS for tablets too. So it is not a clear test for mobile. u agree?
...

Exactly — I completely agree. ✅
  • Both Android and iOS tablets report user-agent strings very similar to phones.

    • Example: An iPad in Safari often has iPad or even iPhone in older iOS versions, or “Macintosh” in newer ones with desktop mode.

    • Android tablets usually include Android in the UA, just like phones.

So just checking the user-agent cannot reliably distinguish phones from tablets.

  • That’s why a combination of user-agent + screen width (or maxTouchPoints) is often used in practice.

    • Phones: narrower width (≤ 768px)

    • Tablets: wider width (> 768px)

  • Even then, it’s still a heuristic — no method is 100% guaranteed.

In short: UA alone is not a clear or foolproof test for mobile phones vs tablets.

...

I think I will go with width > height AND height being less than some number that is suitable for mobile but not tablet.
...

I plan to use this:
  const dimensions = useWindowDimensions();
  const isMobileWebLandscape = Platform.OS === 'web' && dimensions.width > dimensions.height && dimensions.height < 500;
--------------
You see, my Samsung M21 shows 412 px (viewport px something not physical px) height in landscape mode

https://www.browserstack.com/guide/common-screen-resolutions says that in 2025:
Top Common Screen Resolutions Worldwide

Desktop: 1920×1080, 1366×768, 1536×864, 1280×720
Mobile: 360×800, 390×844, 393×873, 412×915
Tablet: 768×1024, 1280×800, 800×1280, 820×1180
--------------
So in mobile landscape mode, the height will typically be less than 500 px (viewport px).
Yr view please.
...

✅ Excellent reasoning — your approach and justification are solid.

Let’s analyze your detection line:

const dimensions = useWindowDimensions();
const isMobileWebLandscape =
  Platform.OS === 'web' &&
  dimensions.width > dimensions.height &&
  dimensions.height < 500;

👍 Why this is a sensible heuristic

  1. Platform.OS === 'web' – correctly limits detection to web builds.

  2. width > height – identifies landscape orientation.

  3. height < 500 – cleverly distinguishes mobiles in landscape from tablets or desktops.

📱 Real-world match

Your Samsung M21’s 412 px landscape height fits this perfectly.
Based on current (2025) viewport metrics:

  • Mobiles in landscape → 360–450 px height

  • Tablets in landscape → 600–800 px height

  • Desktops → typically >700 px height

So < 500 cleanly captures almost all phones while excluding tablets/desktops.

⚙️ Recommendation

You might want to define the threshold as a named constant for readability and easy tuning later:

const MOBILE_LANDSCAPE_MAX_HEIGHT = 500;
const isMobileWebLandscape =
  Platform.OS === 'web' &&
  dimensions.width > dimensions.height &&
  dimensions.height < MOBILE_LANDSCAPE_MAX_HEIGHT;

This makes it clear why the cutoff exists, and lets you tweak it if a specific device behaves differently.

✅ Summary

Your heuristic:

  • Works reliably across 2025’s common devices

  • Avoids false positives from tablets/desktops

  • Is simple and efficient (no dependency on user-agent parsing)

So yes — go ahead with this method. It’s a well-reasoned, practical, and future-resilient solution for detecting mobile web landscape mode.

...
An overview of what I did in past hour or so perhaps: I made changes to two files to get a sort-of optimal solution to this problem: On mobile web landscape mode the lesson screen's video element/control was very small.
So I first figured out how to detect that the device is using mobile web landscape mode (which I discussed above with you).
Next, if device is using mobile web landscape mode, I reduced the width of the permanently open drawer and increased the video height to 60% (from previous 40%).
This has made a significant improvement in lesson screen UI in mobile web landscape mode as the video component is no longer too small. Some lesson names in the drawer have last part of their names clipped but that's an acceptable compromise. There is a minor issue of lack of vertical scrolling in lesson screen due to which the button at the bottom is vertically clipped in mobile web landscape mode.
The behaviour of the app for desktop web, mobile web portrait mode as well as Android (Native) app is not changed.

Commit: Mobile web landscape mode: Increased lesson screen's video height and reduced drawer width to improve screen UI; Need to add vertical scrolling to lesson screen
....

[Then we used ScrollView instead of View in lessons screen. Parts of related exchange is given below.]

Commit: Added vertical scroll to lesson page; Initial testing is OK but needs more testing

09:18, 19 October 2025
[CG explanation on RN ScrollView and View differences.]

Me: i thought view and scrollview should handle the styling the same way.

CG: That’s a very natural assumption — but in React Native (and React Native Web), View and ScrollView differ in how they handle flex and layout. Here’s the key difference that caused what you observed 👇

🧩 1. View layout behavior

A plain <View>:

  • Fully participates in Flexbox layout.

  • Its children are laid out normally and fill space according to flex rules.

  • If you set flex: 1 on the View or its parent, it expands to fill available space.

So your “video + button at bottom” layout works perfectly in a View.


🌀 2. ScrollView layout behavior

A <ScrollView> is special:

  • It creates its own scrollable content container inside.

  • The outer ScrollView fills space like a normal View,
    but its inner content container (the scrollable part) sizes itself to fit the content.

  • That means if your children don’t naturally fill the height, it collapses — and flex behavior doesn’t apply as expected.

That’s why when you switched from ViewScrollView, the layout “broke” — the internal scroll container didn’t stretch to fill the height.


💡 3. Why contentContainerStyle={{ flexGrow: 1 }} fixes it

flexGrow: 1 tells the content container to expand and fill all available vertical space, even if its children don’t require it.
So:

  • When content is short → it fills the screen properly.

  • When content is long → it scrolls correctly.


✅ Bottom line

  • View → static layout (no scroll)

  • ScrollView → dynamic height, needs contentContainerStyle={{ flexGrow: 1 }} to behave like View

So yes — they look similar but their internal layout models differ subtly but importantly.
Your final version (with flexGrow: 1) makes the ScrollView behave like a full-height View plus scrolling support — the best of both worlds.


Me: Hmm. Plain React web does not have this issue, isn't it? Do we have a ScrollView in plain React?

CG: Excellent question — and you’re absolutely right to notice that this issue is specific to React Native (and React Native Web), not plain React for the browser.

Let’s unpack that clearly 👇


🧱 In plain React (web)

There’s no ScrollView component built in.
You just use regular HTML elements, mainly <div> or <section>, and make them scrollable with CSS:

<div style={{ overflowY: 'auto', height: '100vh' }}>
  {/* long content here */}
</div>

✅ This works perfectly because:

  • HTML layout uses block and CSS box model, not React Native’s Flexbox abstraction.

  • The scrollable area and the layout sizing are handled directly by the browser.

  • You don’t need a separate contentContainerStyle — the div itself is the scroll container.


📱 In React Native / React Native Web

ScrollView is a special React Native abstraction, designed to work across Android, iOS, and Web.
It simulates the scrollable div concept but with native-style behavior:

  • It renders a nested structure internally (an outer container + inner scroll content).

  • That inner content does not automatically fill its parent, because in native mobile rendering engines, scrollable areas are meant to size themselves to content first.

Hence why you need:

contentContainerStyle={{ flexGrow: 1 }}

—to mimic a “full height scrollable div” layout.


🧭 Summary comparison

Platform Scroll Container Behavior Needs flexGrow: 1?
Plain React (Web) <div style={{ overflowY: 'auto' }}> Natural scroll & layout ❌ No
React Native <ScrollView> Scroll container wraps content ✅ Yes
React Native Web <ScrollView> abstraction → <div> internally Mimics RN layout quirks ✅ Yes

So yes — you’re absolutely right:
👉 Plain React web doesn’t have this problem because it uses the browser’s natural scroll model.
The ScrollView abstraction introduces this complexity so the same code can behave consistently on both mobile and web.


------------

Me: About why I am making some effort to improve UX for mobile web landscape:
Even if Android app is available as an APK (but not on Play Store), some users may not want to install the app (especially as APK has to be side-loaded). 
Also some mobile users may be on iOS mobiles or even tablets but I don't have an iOS version of the app.
So some users may want to explore this app via mobile browser.

Now in mobile browser, portrait mode is quite OK for some app functionality.
But a key app functionality is playing the lesson video. On mobile, most users would want to play it full screen. Full screen mode on mobile switches automatically to landscape mode (as that is more suitable for typical aspect ratio videos I guess). So the user has to rotate the device. 
Now if the user stops the video and wants to navigate to another lesson using left drawer, as the device is in landscape mode now, it shows the mobile web landscape view. User will find it convenient to operate it in this landscape view itself instead of rotating device to portrait mode, choose another lesson, click full screen which takes device back to landscape mode making user rotate device again.
But if the mobile web landscape UX is reasonable, user could navigate through various lessons of the course (or another course) on landscape mode itself without having to do lot of device rotation back-and-forth.

What's ur view of my above analysis? Please be frank as I am new to mobile app development and so could be way off the mark here.

CG: Your analysis is actually very sound and practical, and shows good empathy for real users. Here’s a breakdown:
  1. APK vs browser: You correctly identified that not all users will install a side-loaded APK. iOS users can’t install your app at all yet, and some Android users may avoid side-loading. So mobile web access is important. ✅

  2. Portrait mode usability: Portrait works for some functionality, but for videos, the natural aspect ratio is landscape, so full-screen playback almost always rotates the device. Users expect this. ✅

  3. User frustration with rotation: You’re thinking ahead about friction. If a user rotates to landscape to watch a video, then has to rotate back to portrait just to navigate the lessons, that’s extra effort and frustration. Making the landscape UX usable for navigation is smart UX thinking. ✅

  4. Mobile web landscape UX: Your solution—reducing drawer width, increasing video height, etc.—is a reasonable compromise. Some drawer text gets clipped, but the core functionality (video viewing + lesson navigation) is preserved. That’s exactly how you handle UX trade-offs.

  5. General mobile app UX principle: The principle you’re following is “reduce unnecessary user effort and context switching.” You want the user to stay in the same device orientation as much as possible while completing the task. That’s a strong UX guideline.

In short: you are on the right track. Mobile web landscape is an edge case that matters for video-heavy apps, and your reasoning is not overcomplicated—it's practical. As long as the layout doesn’t break for desktop, tablet, or Android portrait, your approach is solid.

...


// @ts-ignore only ignores the very next line. It does not affect the whole file.

Example:

// @ts-ignore
label={() => <Text title={lesson.name}>{lesson.name}</Text>} // TypeScript will ignore error here

// This line below will still be checked normally
console.log("This will still show type errors if any");

It’s a line-level directive.

====================

Me: If we use:
label={lesson.name}
(without a function), can we pass some additional prop or whatever to say, give me multiple lines version when needed (no truncation)?

CG: No — if you just use label={lesson.name} in a DrawerItem, you can’t pass additional props like numberOfLines or style, because the label prop in that form only accepts a string (or something coercible to string). That’s why React Navigation (and the underlying react-native-paper/react-native-drawer behavior) always truncates text if it doesn’t fit.

To get multi-line labels, you must provide a function that returns a <Text> element, like you did:

label={() => (
  <Text style={{ /* optional styling */ }}>
    {lesson.name}
  </Text>
)}

This is the only way to pass Text props (multi-line, font size, color, etc.) to the label.

So yes — using the function form is the standard approach when you want multi-line or styled labels.

================

Commit: Improved mobile web landscape UI - menu bar, drawer labels


Me: A strange issue which I think is appearing only after using ScrollView instead of View in lesson screen.
On desktop PC window width increasing beyond 1200 px, the window height increases with the bottom button starting to go off screen and eventually goes off screen. The vertical scrollbar allows user to scroll down to access the bottom button. 
Why is this happening above 1200px? I don't think I have any NativeWind code for such a breakpoint.

[Longish exchange. Eventually some debugging using Chrome DevTools provided styling solution. ...]

Me: ... So I did some web style debugging. In Chrome DevTools for the outer div corresponding to the scroll view, it shows:
<div class="css-view-175oi2r" style="flex-grow: 1;justify-content: space-between;"><video controls="" crossorigin="anonymous" ...
-------------

In DevTools style:
.css-view-175oi2r has 
flex-shrink: 0

I unchecked flex-shrink: 0
After that the problem gets fixed, at least in this round of testing. No matter how small or wide, the width of the window on desktop PC, as the text content is limited, the bottom button is always shown on the screen and at the right place - bottom.

If I check flex-shrink: 0 again, the problem appears again with bottom button going off screen at large desktop window width (> 1200 px). 

CG: Ah — now it makes sense! This is a classic web vs React Native style nuance.

Here’s what’s happening:

  1. ScrollView contentContainerStyle creates a wrapper <div> on web.

  2. flexGrow: 1 is applied, but React Native for Web automatically adds flexShrink: 0 to the inner container.

  3. When flexShrink: 0, the container refuses to shrink when the viewport gets tall/wide — it stretches beyond the visible height if content is short. That’s why your bottom button goes off-screen on large windows.

  4. Unchecking flexShrink: 0 in DevTools allows the container to shrink to fit the viewport, so justifyContent: 'space-between' works correctly and the button stays at the bottom.

Takeaways / fixes:

  • On web, you need flexShrink: 1 (or remove flexShrink: 0) in contentContainerStyle.

  • On native, flexShrink usually doesn’t matter, so this fix is safe.

  • React Native / NativeWind doesn’t expose flexShrink directly in Tailwind classes for web, so you’ll need to add it via style prop.

For example, in your ScrollView:

<ScrollView
  contentContainerStyle={{
    flexGrow: 1,
    flexShrink: 1,  // <= crucial for web to prevent oversizing
    justifyContent: 'space-between',
  }}
  showsVerticalScrollIndicator={true}
  style={{ flex: 1 }}
>
  {/* video + notes + button */}
</ScrollView>

This should consistently fix the large desktop width issue while keeping the bottom button visible when content is short, and still allow scrolling when content is long.

...

Me: That seems to have fixed that problem on web. Notice a minor issue on Android native where the bottom button's bottom part is slightly clipped vertically. So its blue padding below the text of the button is not shown in Android app whereas on web mobile portrait it is shown properly (without vertical scrollbar).

CG: Ah yes — that’s a subtle but common issue. It usually comes from safe area insets on Android (and iOS) not being fully accounted for. On mobile devices, the bottom of the screen may have system navigation bars or insets, so your button can get partially hidden if the padding doesn’t include that.

In your code, you are already using pb-safe in Tailwind (pb-safe → paddingBottom safe area) for the button, ...

...

Me: This works:

          className="bg-primary py-3 flex-row items-center justify-center gap-2"

But I don't know if it is a proper solution.

CG: Yes — that’s actually fine! 🙂

Here’s why it works and why it’s acceptable:

  • py-3 sets equal top & bottom padding, so the text and icon are vertically centered.

  • flex-row items-center justify-center ensures horizontal layout and vertical alignment of the children.

  • gap-2 handles spacing between the text and the icon.

What you were running into before (with py-5 mb-4 pb-safe) was just overlapping/mismatched padding that shifted the content. By using only py-3, you get a clean, balanced layout.

If your only concern is Android clipping a tiny bit at the bottom due to safe area, that’s a minor issue and can be optionally handled with paddingBottom: Platform.OS === 'android' ? 16 : 16 + safeAreaInsetBottom in style.

So yes — for most cases (especially if you’re okay with the tiny Android clipping), your current py-3 flex-row items-center justify-center gap-2 is a proper and simple solution.

...

Commit: Lesson page: Fixed desktop PC wide window bottom button out of view issue; Android bottom button bottom clipping issue

-----------

On mobile web landscape view and also desktop web view for last few lessons, moving to next lesson should, if required, scroll current lesson in open drawer into view.

[CG gave some complex code proposed solution for both Android and web.]

Me: On Android, as I am testing it now only with mobile, I don't have this issue as the Android app is always in Portrait mode and the mobile width is lesser than needed for permanent drawer. So drawer is collapsed and has to opened by user and closed to continue with main lesson page.

I am not testing or worrying about Android tablets whose width may be wide enough for app to show drawer permanently. So we can ignore this condition, as of now at least.

But I need it to be handled on web. So I am very interested in simple solution for web.

[CG provided some code ... some iterations ... After we got it working, I asked CG for a summary note.]

CG: 

Summary: Auto-scrolling active lesson into view in Drawer

🎯 Goal

Ensure that whenever the Drawer is open, the currently active lesson is automatically scrolled into view — particularly useful on web when:

  • the lesson list is long, and

  • the user switches to landscape mode (e.g., mobile web landscape view with permanent drawer).


⚙️ Implementation Steps

  1. Created a ref for the active lesson element

    const activeLessonRef = useRef<HTMLDivElement | null>(null);
  2. Attached the ref only to the active lesson

    label={() => { if (Platform.OS === 'web') { return ( <div ref={isActive ? activeLessonRef : null}> <Text className="dark:text-white">{lesson.name}</Text> </div> ); } else { return ( <View> <Text className="dark:text-white">{lesson.name}</Text> </View> ); } }}
  3. Used useEffect to scroll it into view on changes

    useEffect(() => { if (Platform.OS === 'web' && activeLessonRef.current) { activeLessonRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', }); } }, [pathname, isMobileWebLandscape]);
    • pathname → ensures scrolling occurs when the user navigates to a new lesson.

    • isMobileWebLandscape → ensures scrolling happens again when switching from portrait to landscape mode (where the drawer becomes visible).

    • Using block: 'nearest' avoids excessive movement and keeps UX smooth.


🧩 Behavior

  • On mobile web (portrait): drawer is hidden; no scrolling needed.

  • When switching to landscape (drawer visible): active lesson auto-scrolls into view.

  • When navigating between lessons: active lesson always comes into view smoothly.


Result

A polished, platform-aware drawer experience that:

  • works seamlessly on both web and native,

  • auto-adjusts on orientation change, and

  • maintains user context (current lesson always visible).


...

Commit: Web: moving to next lesson ensures current lesson is shown in open drawer

================================================

19:16, 19 October 2025, Exploring giving full screen support on mobile to app 


Me: Chrome browser on mobile does not seem to have full screen mode

CG: You're right — Google Chrome on mobile (Android or iOS) does not offer a true “fullscreen mode” like desktop browsers do (where you can press F11).

Here’s a breakdown 👇

🔍 1. What Chrome mobile actually supports

  • Chrome automatically hides the address bar when you scroll down a webpage.

    • This gives a semi-fullscreen effect, but the status bar and navigation bar (at the top and bottom) usually remain visible.

  • There is no built-in button or setting for a persistent fullscreen mode.

⚙️ 2. Workarounds

✅ Option A — “Add to Home Screen” (best native way)

If the website is PWA-ready (Progressive Web App):

  1. Open the website in Chrome mobile.

  2. Tap ⋮ (menu)Add to Home screen.

  3. Launch it from the home screen shortcut.
    → It opens in a standalone fullscreen-like window (no address bar or browser UI).

If your site uses a proper Web App Manifest with:

"display": "standalone"

or

"display": "fullscreen"

then it will appear truly fullscreen when launched this way.

✅ Option B — Developer / App-level fullscreen (for your own web apps)

If you control the webpage (e.g., your React or Expo web app), you can use:

document.documentElement.requestFullscreen();

— but note:

  • Works only after a user gesture (like tapping a button).

  • May still show system status bars depending on Android/iOS version and browser policies.

⚠️ Option C — Third-party browsers

Some alternative Android browsers (like Kiwi, Samsung Internet, or Fully Kiosk Browser) support real fullscreen toggling — but this is not available in Chrome itself.


Would you like me to show you how to make your own web app (like your React site) open in fullscreen when added to home screen (via manifest.json and small JS tweak)? That’s the most effective method for Chrome mobile.


Me: Yes, mobile chrome does hide the address bar when I scroll down a webpage like a blog post.

But I have a React Native and Expo web app. I want to have this full screen feature in that app. In that app when I have a screen with a menu bar at the top, and have vertically scrollable content below the menu bar, scrolling down on that content does NOT hide mobile Chrome's address bar.

I don't think that React Native creates a PWA app by default like something like Create-React-App does for plain React apps.

[Rest of exchange on this topic till summary note below has been moved to Details section.]

[CG summary note:]

Summary: PWA / Fullscreen Attempt on Expo SDK 52 + Metro

Context:

  • Project uses Expo SDK 52, Expo Router, and Metro bundler.

  • Goal: enable fullscreen (standalone) mode on mobile Chrome when the app is added to the home screen.

Steps Taken:

  1. Updated app.json "web" section with recommended PWA fields:

"display": "standalone", "themeColor": "#0d6c9a", "backgroundColor": "#ffffff", "shortName": "LMS", "startUrl": "."
  1. Built and deployed the app using npm run export-web and EAS Hosting (HTTPS URL).

  2. Opened deployed URL on mobile Chrome → Add to Home screen → launched from home screen icon.

Result:

  • The app opened in Chrome browser with address bar visible, not in fullscreen/standalone mode.

  • Confirmed that the deployed app reflects latest functionality.

Analysis / Reason for Failure:

  • Chrome requires a fully static PWA (manifest + icons + optional service worker) to trigger standalone fullscreen.

  • Metro + Expo Router produces a runtime manifest, but does not generate a full static PWA build.

  • Expo SDK 52 does not have a stable @expo/webpack-config to create a static PWA export.

  • Without a static manifest and service worker, Chrome treats the app as a normal web page, not a PWA.

Conclusion:

  • With the current setup (Expo SDK 52 + Metro + Expo Router), true fullscreen on mobile Chrome is not achievable without additional PWA configuration or upgrading to a newer Expo version with stable Webpack PWA support.

  • Further work (manifest.json, service workers) would be required to pursue fullscreen.

  • Decided to suspend PWA/fullscreen effort to avoid extra complexity.

===================================================
===================================================

Details

Me: Yes, mobile chrome does hide the address bar when I scroll down a webpage like a blog post.

But I have a React Native and Expo web app. I want to have this full screen feature in that app. In that app when I have a screen with a menu bar at the top, and have vertically scrollable content below the menu bar, scrolling down on that content does NOT hide mobile Chrome's address bar.

I don't think that React Native creates a PWA app by default like something like Create-React-App does for plain React apps.

CG: Perfectly understood 👍 — and you’re right on all points.

Let’s unpack this clearly since it’s a subtle difference between React (web) and React Native Web via Expo.


🧩 The situation

In Expo for Web, your app is rendered inside a single full-height React Native-style container (<div id="root"> or similar) that uses height: 100% and overflow: hidden.
That layout style prevents Chrome mobile from detecting “scrolling the page itself” — so Chrome never hides its address bar.

So the scroll is happening inside your app’s scroll container, not in the browser’s main scrollable area.

Chrome only auto-hides the address bar if the top-level document scrolls vertically.


✅ Option 1 — Easiest workaround (let document scroll)

If your app has one main vertically scrollable screen (like a blog feed or long content view):

  1. Ensure that your root container allows the document body to scroll instead of internal views.

  2. Avoid styles like:

    html, body, #root {
      height: 100%;
      overflow: hidden;
    }
    

    Instead, use:

    html, body {
      height: auto;
      min-height: 100%;
      overflow-x: hidden;
    }
    
    #root {
      min-height: 100vh;
    }
    

This lets Chrome’s scroll detection kick in, so its address bar hides naturally as the user scrolls down.

Tradeoff:
If your app relies on internal ScrollView components (React Native style), you’ll have to refactor those into native browser scroll containers (overflow: auto divs that let the page itself scroll).


✅ Option 2 — Make your Expo web app a PWA

You can get a true fullscreen “installed app” feel when opened from the home screen.

Steps:

  1. In your Expo project, open app.json (or app.config.js).

  2. Add this under "web":

    "web": {
      "display": "standalone",
      "themeColor": "#000000",
      "backgroundColor": "#ffffff"
    }
    
  3. Run:

    npx expo export:web
    

    or

    npx expo build:web
    
  4. Serve that build and open it in Chrome mobile.

  5. Tap ⋮ → Add to Home screen.
    When you open from the shortcut, it runs fullscreen (no address bar at all).


✅ Option 3 — Manual fullscreen via JavaScript (limited)

You can give the user a “Go Fullscreen” button that calls:

document.documentElement.requestFullscreen();

Example:

const goFullScreen = () => {
  if (document.documentElement.requestFullscreen) {
    document.documentElement.requestFullscreen();
  }
};

👉 But Chrome mobile only allows this after a user gesture (tap/click).
👉 It also doesn’t persist — when the user navigates away, fullscreen exits.


🧠 Recommendation for your case

Since your app is an Expo web app, the best long-term solution is:

  • Option 2: Make it a PWA with "display": "standalone".

    • Works cleanly.

    • No browser UI.

    • True fullscreen experience.

  • For now, while testing in Chrome mobile directly, you can temporarily try Option 1 by allowing page scroll.


Would you like me to show you exactly how to modify your Expo app.json and confirm whether your build already has a manifest.json generated (so that the PWA fullscreen works)?


Me: Option 2 is interesting ... But first about Option 1. My app is based on a tutorial that I am coding-along with. It uses NativeWind and has the following global.css file which is imported in at a top level in the app: [I shared the global.css file contents.]

CG: Good — that helps clarify what’s in your current CSS setup.

So right now, your global.css (used with NativeWind and Tailwind) doesn’t include any explicit styles for htmlbody, or #root.
That means Expo’s default web template is likely adding those for you — and by default, Expo for Web does this internally:

html, body, #root {
  height: 100%;
  overflow: hidden;
}

That’s what’s preventing the mobile Chrome address bar from hiding when scrolling, because the page itself never scrolls.


✅ Here’s what you can do (Option 1 tweak)

Add this at the bottom of your global.css file to override those defaults:

html, body {
  height: auto !important;
  min-height: 100%;
  overflow-x: hidden;
  overflow-y: auto;
}

#root {
  min-height: 100vh;
  height: auto !important;
  overflow: visible !important;
}

🧩 Why this works

  • height: auto lets the document grow taller than the viewport as your content grows.

  • That makes the page itself scrollable, not just your internal React Native Web scroll containers.

  • Once Chrome detects page-level scrolling, it will auto-hide its address bar when you scroll down.


⚠️ Important Notes

  • If your components use React Native’s <ScrollView> (which sets its own overflow), Chrome still won’t hide the bar — because the document isn’t scrolling, the div inside is.

  • You’d need to either:

    • Let your main screen use normal browser scrolling (overflow-y: auto on the body or root div),

    • Or explicitly make the outermost container tall (like min-h-[200vh] for testing) just to confirm the effect.


Would you like me to suggest the minimal CSS + layout change (in your top-level component) so that your main scrollable screen uses browser scrolling instead of <ScrollView>—so we can test whether Chrome hides the address bar?


Me: My app does use ScrollView - So I think Option 1 is not suitable for me. I should try Option 2. 

CG: Exactly 👍 — that’s the right call.

Since your Expo web app uses <ScrollView>, Chrome won’t detect “page scroll,” so Option 1 can’t trigger address-bar hiding.
Option 2 (PWA mode with "display": "standalone") is the clean, reliable way to get a true fullscreen experience.

Let’s go step by step 👇


✅ Step 1 — Add PWA settings in app.json

Open your app.json (or app.config.js if you’re using that) and look for the "expo" object.
Inside it, add a "web" block like this:

{
  "expo": {
    "name": "YourAppName",
    "slug": "your-app-slug",
    "version": "1.0.0",
    "web": {
      "favicon": "./assets/favicon.png",
      "display": "standalone",
      "themeColor": "#000000",
      "backgroundColor": "#ffffff"
    }
  }
}

👉 "display": "standalone" removes Chrome’s address bar and tab UI when launched from the home screen.
(If you want absolute fullscreen — even system status bars hidden — you can later change it to "fullscreen", but “standalone” feels more natural.)


✅ Step 2 — Build the web version

Run in your project root:

npx expo export:web

or, if you’re testing locally:

npx expo start --web

When exported, Expo automatically generates a manifest.json in the web-build/ folder using the web config above.


✅ Step 3 — Serve and install it

  1. Serve the web-build folder locally:

    npx serve web-build
    
  2. Open the local URL (e.g., http://192.168.x.x:3000) on your Android Chrome browser.

  3. Tap ⋮ → Add to Home screen.

  4. Launch it from the new icon on your home screen.

You’ll now have:

  • ✅ No Chrome address bar.

  • ✅ No tabs or URL bar.

  • ✅ A native-like fullscreen app.


🧠 Step 4 (optional) — Confirm manifest generation

Open your web-build/manifest.json and ensure it has entries like:

{
  "display": "standalone",
  "start_url": ".",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "short_name": "YourAppName"
}

Once you confirm it works, you can later fine-tune icons, splash screen, or start URL for a fully polished PWA look.

Would you like me to show you the recommended extra fields to add (for icons, name, and splash colors) so it looks like a real installed app on Android?

------------

[Then I changed app.json as suggested by CG.]

Ran: npx expo export:web
Got the error:
CommandError: expo export:web can only be used with Webpack. Use expo export for other bundlers.
---------

CG suggested change in app.json
From:
    "web": {
      "bundler": "metro",
      "output": "server",
  ...
To
"web": {
  "bundler": "webpack",
  "output": "static",
  ...
-------
Ran: npx expo export:web

lms> npx expo export:web
CommandError: It looks like you're trying to use web support but don't have the required dependencies installed.

Please install @expo/webpack-config by running:

npx expo install @expo/webpack-config

If you're not using web, please ensure you remove the "web" string from the platforms array in the project Expo config.

lms> 
-----------
Ran: npx expo install @expo/webpack-config

lms> npx expo install @expo/webpack-config
>> 
env: load .env
env: export EXPO_PUBLIC_STRAPI_API_URL EXPO_PUBLIC_WEB_SOON_URL EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY EXPO_PUBLIC_REVENUECAT_WEB_KEY STRAPI_API_ADMIN_TOKEN
› Installing 1 other package using npm
> npm install --save @expo/webpack-config
npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree
npm error
npm error While resolving: lms@1.0.0
npm error Found: expo@52.0.47
npm error node_modules/expo
npm error   expo@"~52.0.46" from the root project
npm error   peer expo@"*" from @expo/dom-webview@0.2.7
npm error   node_modules/@expo/dom-webview
npm error     peerOptional @expo/dom-webview@"*" from expo@52.0.47
npm error   1 more (@expo/metro-runtime)
npm error
npm error Could not resolve dependency:
npm error peer expo@"^49.0.7 || ^50.0.0-0" from @expo/webpack-config@19.0.1
npm error node_modules/@expo/webpack-config
npm error   @expo/webpack-config@"*" from the root project
npm error
npm error Fix the upstream dependency conflict, or retry
npm error this command with --force or --legacy-peer-deps
npm error to accept an incorrect (and potentially broken) dependency resolution.
npm error
...
------------

[Seems like latest version of @expo/webpack-config does not support Expo 52 which itself is many months old]

Me: You mean latest version of @expo/webpack-config does not support Expo 52 which itself is many months old?

CG: Yes — you’re correct. What you’re seeing is exactly the problem: the latest version of @expo/webpack-config (version 19.0.1) expects expo to be at version ^49.0.7 or ^50.0.0-0. (Reddit)

So when you’re using Expo SDK 52 (which is a newer version), you run into a peer-dependency conflict — npm notices the mismatch and refuses to install unless you force it or ignore peerDeps.


✅ What this means for you

  • If you install @expo/webpack-config@19.0.1 while using Expo SDK 52, you’ll get this sort of error because it lists:

    peer expo@"^49.0.7 || ^50.0.0-0" from @expo/webpack-config@19.0.1 (Reddit)

  • Also, the package’s own documentation says that "Expo Webpack is in maintenance mode" and they’ve been focusing on a new universal bundling solution. (npm)


🔍 Your options

Given this mismatch, you have a few paths:

  1. Force install anyway
    You can install @expo/webpack-config with --legacy-peer-deps or --force, e.g.:

    npm install --save @expo/webpack-config --legacy-peer-deps
    

    This may work, but you may run into subtle build issues or unsupported behaviour because you’re outside the officially declared peer-version range.

  2. Use the Metro bundler / native Expo Router web output instead
    Since you’re on SDK 52 (and you already used "bundler": "metro" for web in your config), it might be more stable to continue using the Metro web build or whatever Expo recommends for SDK 52 rather than forcing Webpack. The documentation indicates Expo is steering new projects away from Webpack. (Expo Documentation)

  3. Downgrade to an Expo version that aligns with the Webpack config peer-range
    If you really need Webpack + @expo/webpack-config to work cleanly, you could use Expo SDK 50 (or whichever version the config supports) — but that might require upgrading/adjusting a lot of other libraries, so it’s more disruptive.

  4. Wait / upgrade to the newer bundling solution when Expo supports it
    Since Expo indicates the Webpack config is in maintenance mode and they’re working on a new universal solution, you could keep using the current Metro setup, and when Expo releases the newer solution (or upgrades @expo/webpack-config for newer Expo SDKs), you can migrate then.


🧾 Recommendation for your case

Since you are already on Expo SDK 52 and have a setup using Expo Router + Metro (for web bundling), I’d suggest going with Option 2 above:

  • Keep "bundler": "metro" in your web config.

  • Skip installing @expo/webpack-config unless you really need the Webpack static export path.

  • Use Expo’s recommended build path for web + PWA (or whichever is supported) rather than forcing the Webpack route.

If you do want to proceed with Webpack anyway (for PWA static export) and are comfortable accepting possible mismatches, you can install with --legacy-peer-deps, test thoroughly, and watch for issues.


If you like, I can check exactly what versions of @expo/webpack-config support Expo SDK 52 (or what workarounds people are using) and send you those version numbers & steps. Would you like me to dig into that?

---------

[So I am switching to Option 2 which is using Metro bundler itself.]
--------------

[IFIRC]
app.json changes made (after output server entry):
    "web": {
      "bundler": "metro",
      "output": "server",
      "favicon": "./assets/images/favicon.png",
      "display": "standalone",
      "themeColor": "#0d6c9a",
      "backgroundColor": "#ffffff",
      "shortName": "LMS",
      "startUrl": "."
    },
---------------

[IFIRC, I then ran the server the usual way: npm run start]
Opening Home screen icon opens app in Chrome browser with address bar shown - not full screen.

CG: Ah — now it makes sense why it’s not working. Let’s carefully go through the likely causes.

With Expo SDK 52 + Metro, “Add to Home screen” on mobile Chrome does not always guarantee true fullscreen unless a few conditions are met:


1️⃣ PWA manifest must actually exist and be served

  • Metro serves a runtime manifest for development, but Chrome sometimes ignores it if the URL is localhost, or if the manifest isn’t properly linked in <head>.

  • Check your dev server URL: http://192.168.x.x:19006 may not trigger Chrome’s PWA detection properly in dev mode.

✅ Workaround: test using a static build hosted on a local network (like npx serve web-build) rather than npx expo start --web.


2️⃣ Chrome’s “Add to Home screen” rules

Chrome only opens in standalone fullscreen if all of the following are true:

  1. The site has a valid manifest.json with:

    {
      "display": "standalone",
      "start_url": ".",
      "name": "LMS",
      "short_name": "LMS",
      "background_color": "#ffffff",
      "theme_color": "#0d6c9a",
      "icons": [ ... ]
    }
    
  2. The manifest is linked in <head> of the page:

    <link rel="manifest" href="/manifest.json" />
    
  3. The page is served over HTTPS (or localhost).

During development with Metro, the runtime manifest may not meet all these conditions, so Chrome just opens it in the normal browser tab.


3️⃣ ScrollView prevents Chrome auto-hiding

Even if the app opens as standalone, if your content is inside <ScrollView> with overflow: scroll, Chrome won’t detect “page scroll.”

  • That’s fine — with PWA standalone mode, Chrome removes the address bar anyway.

  • But if Metro doesn’t generate a proper manifest, Chrome never treats it as a standalone PWA.


✅ Recommended approach

  1. Export a static PWA build (this ensures manifest.json is correctly created and linked):

    • Switch "web": { "bundler": "webpack", "output": "static" } temporarily in app.json.

    • Install @expo/webpack-config (use --legacy-peer-deps if needed).

    • Run:

      npx expo export:web
      npx serve web-build
      
    • Open that local URL on Chrome mobile → Add to Home screen → Launch → should be fullscreen.

  2. Keep Metro for dev, Webpack only for PWA export.

    • You don’t need to migrate your ScrollViews.

    • You get a real PWA experience, including fullscreen.


💡 Bottom line:

With Metro dev server, Chrome’s “Add to Home screen” cannot reliably open in fullscreen.
To test fullscreen PWA, you need a static Webpack export (or a hosted HTTPS site with manifest.json) — that’s the only reliable way for Chrome to open your app standalone.

...


CG: Apologies for any confusion earlier. Upon reviewing the provided articles, they do not offer a quick and easy solution to achieve fullscreen behavior in your Expo SDK 52 web app without utilizing Webpack. The primary method to achieve fullscreen behavior on mobile browsers is by configuring your app as a Progressive Web App (PWA) with a valid manifest file and serving it over HTTPS. This approach typically requires using Webpack for the build process.

Given your constraints and the desire to avoid Webpack, achieving true fullscreen behavior in mobile browsers may not be feasible with Expo SDK 52 and the Metro bundler alone. The Expo team has acknowledged limitations in this area, particularly with the handling of browser chrome on mobile devices. GitHub

If you are open to exploring alternative solutions, such as migrating to a more recent version of Expo that supports Webpack or considering other frameworks that offer better PWA support, I would be happy to assist you in evaluating those options.

Me: Webpack beta is the problem. Not webpack itself. I don't want to use any beta or canary stuff.

...

CG: Apologies for any confusion in my earlier responses. Upon reviewing the information, it appears that Expo SDK 53 does not provide stable support for @expo/webpack-config. The Expo team has deprecated @expo/webpack-config and recommends migrating to Expo Router for building web applications. (Expo Documentation)

Given this, achieving fullscreen behavior in mobile Chrome without using Webpack or Expo Router may not be feasible with Expo SDK 53. The recommended approach for implementing fullscreen behavior in mobile browsers is by configuring your app as a Progressive Web App (PWA) with a valid manifest file and serving it over HTTPS, which typically requires using Expo Router or a similar solution.

If you are open to exploring alternative solutions or need assistance with migrating to Expo Router, I would be happy to guide you through the process.

Me: My package.json deps already has:

    "expo-router": "~4.0.20",

...

CG: Ah — right! In Expo SDK 52+ with Expo Router, the old expo build:web command is no longer supported in the local CLI.

The supported command is:

npx expo export:web

…but here’s the catch:

  • With your current Metro bundler setup, expo export:web only works with Webpack.

  • Since you want to avoid any beta/canary @expo/webpack-config, you cannot use expo export:web reliably on SDK 52.

  • That means: there is no stable way to generate a full static PWA for testing fullscreen without either:

    1. Installing a compatible Webpack version (requires beta/canary for SDK 52), or

    2. Upgrading Expo SDK to a version where stable Webpack export is supported (like SDK 53+). [Ravi: CG's message is confusing here as even with SDK 53+, stable Webpack export does not seem to be available. Only beta/canary Webpack export seems to be available. I am skipping the exchanges related to me pointing this out to CG.]


✅ Implication for your app

  • You can continue development with Metro + Expo Router (npx expo start --web).

  • You can Add to Home screen, but in dev mode on Chrome, address bar will still be visible.

  • To get real fullscreen PWA behavior, you will need either stable Webpack export (requires SDK upgrade) or host the app on a proper HTTPS server and let Chrome detect it as a PWA.


💡 Summary of your choices right now

OptionProsCons
Stay on Expo SDK 52 + MetroStable, no betaChrome shows address bar in dev, no reliable fullscreen
Force-install Webpack betaCan run expo export:web and test fullscreenBeta package, potential instability
Upgrade Expo SDKStable Webpack export possible [Ravi: This seems to be inaccurate. Please see related above comment of mine.]Upgrade effort, may require dependency updates

...

Me: I have an earlier version of the app deployed on https://sg-eduapp-lms--bs5bc0lhi7.expo.app/
But that has old app.json without the additions you suggested.
...
Me: I would like to try out deploying my current app after confirming your app.json suggestions, to eas which serves it over https . Then I will load that in my mobile chrome browser and see if I get Full Screen via add to home. ... That would be relatively straightforward to try. 
If that does not work, I will suspend this PWA effort. I do not want to get into manifest.json, service worker and all that stuff that Expo docs talk about for PWA.
What do u think of what I said?

[CG agreed.]
...

Me: Is the below app.json code OK, before I create a web build (to deploy to eas):
{
    ...
    "web": {
      "bundler": "metro",
      "output": "server",
      "favicon": "./assets/images/favicon.png",
      "display": "standalone",
      "themeColor": "#0d6c9a",
      "backgroundColor": "#ffffff",
      "shortName": "LMS",
      "startUrl": "."
    },
    ...
}
------------

[CG was OK with it.]

Me: Ran: npm run export-web
package.json script command that runs: "copy .env .env.back && copy .env.cloud.fs .env && expo export --platform web"
That succeeded.

Ran: eas deploy 

🎉 Your deployment is ready

Dashboard       ---snipped---
-------------

Opened https://sg-eduapp-lms--st52bu6bp9.expo.app in mobile Chrome. Used Add to Home Screen. Closed mobile chrome. Opened created lms on eas app shortcut. It opened in Chrome browser showing address bar.

So this test has NOT worked out. Perhaps more work is needed to enable PWA in the app.

I also confirmed that the app has the latest behaviour (changes that were made recently). So the eas app above is the current functionality web app.

CG: Thanks for the detailed test — this confirms what we expected:
  • Even with your updated app.json and a deployed HTTPS Expo Hosting URL, Chrome still opens the app in a normal browser tab with the address bar.

  • This means Metro + Expo Router alone (without Webpack / static PWA export) is not enough for Chrome to recognize your app as a standalone PWA.

  • In other words, the basic PWA fields in app.json are not sufficient to trigger fullscreen on mobile Chrome in this setup.

...

Comments