Hiding URL bar with Ionic and Mobile Safari

This article describes an alternative approach to hiding the URL bar in Ionic app on Mobile Safari. The end solutions looks like the following demo:
What is the problem
On Mobile Safari, the URL (or address) bar does not shrink when scrolling content in an ion-content
container. This issue is not exclusive to Ionic, other web and single-page-app (SPA) frameworks work the same way. In this below image you can see how much of the screen is occupied by the URL bar.
The URL bar should shrink when Safari detects the <html>
or <body>
elements have their Y-coordinate scroll position changed, even a single pixel change will cause the URL bar to shrink.
And over the years there have been several discussions about this, with the current resolution by framework developers to keep the current behavior. This is a fair outcome since the primary surface for Ionic apps is the native mobile app.
Problems with existing solutions
I have tried all of the proposed solutions mentioned on the Ionic forms, StackOverflow, on Reddit threads, etc. And multiple claim to have solved it, but either I am missing something critical or there is nuance that makes them incompatible with Ionic.
react-ios-scroll-lock: Promising, and worth reviewing if you are reading this. But I could not make the layering approach work with Ionic components.
Using JavaScript scroll events and position: Propagating the scroll events no longer triggers the shrink behavior in newer Mobile Safari versions.
Fake-it using scrollTo: Similar to above, the behavior stopped working in 2020.
Shrinking yourself approach
My proposal is to focus on the components we control in the app and provide maximum screen real estate by shrinking the ion-header
and ion-tab-bar
when a scroll starts.
The following is a short overview of how TopVault achieves this.
AppManagerProvider
This is a React context for managing generic app behavior, and coordinating an action between a page and header and tab bar is a good example of something that is whole-app. So the App
component might look like this:
function App({ children }: { children: [React.ReactElement<typeof IonTabs>, ...React.ReactNode[]] }) {
const [tabs, ...siblings] = children;
return (
<AppManagerProvider>
{tabs}
{siblings}
</AppManagerProvider>
);
}
And an abridged AppManagerProvider
export function AppManagerProvider({ children }: { children: [React.ReactElement<typeof IonTabs>, ...React.ReactNode[]] }) {
const [tabs, ...siblings] = children;
<AppManagerContext.Provider
value={{
setShrinkPageHeaders: (shrinkHeaders: boolean) => setShrinkPageHeaders(shrinkHeaders),
shrinkPageHeaders,
}}
>
<span id="tabs-container" className={shrinkPageHeaders ? 'shrink-tabs' : 'normal-tabs'}>
{tabs}
{siblings}
</span>
</AppManagerContext.Provider>
}
Now, anything in the app can request that the component shrink as well as check if they are currently in a normal or shrink state.
Throughout TopVault, pages that support scrolling use an alternate component instead of ion-content
called ScrollableContent
. And this facilitates showing a helpful “scroll-to-top” button after the user has scrolled slightly. We use the same component to track if the header should shrink:
type ScrollableContentProps = React.ComponentProps<typeof IonContent> & {};
export function ScrollableContent({ children, ...props }: ScrollableContentProps) {
const { setShrinkPageHeaders, shrinkPageHeaders } = useAppManager();
const onScroll = useCallback(
(e: IonContentCustomEvent<ScrollDetail>) => {
if (!shrinkPageHeaders && e.detail.scrollTop > scrollPosition && scrollPosition > shrink_TRIGGER) {
setShrinkPageHeaders(true);
}
if (!shrinkPageHeaders && e.detail.scrollTop < scrollPosition) {
setShrinkPageHeaders(false);
}
setScrollPosition(() => e.detail.scrollTop);
},
[...]
);
return (
<IonContent ref={contentRef} onIonScroll={onScroll} scrollEvents={scrollable} {...props}>
{children}
</IonContent>
);
}
And similarly, the ion-header
is replaced with a PageHeader
for pages.
export function PageHeader({ children }: Props) {
const { shrinkPageHeaders } = useAppManager();
return (
<IonHeader className={shrinkPageHeaders ? 'shrink-header' : 'normal-header'} collapse="condense">
{children}
</IonHeader>
);
}
And to bring it all together, the CSS for the referenced classes:
.normal-header, .normal-header ion-title {
transform: scale(1);
transform-origin: top;
transition: transform 0.3s;
}
.shrink-header {
opacity: 0;
transform: translateY(-30px);
transition: opacity 0.3s, transform 0.3s;
}
.normal-tabs .app-tab-bar {
transform: scale(1);
transform-origin: bottom;
transition: transform 0.3s;
}
.shrink-tabs .app-tab-bar {
transform: scale(1, 0.5);
transform-origin: bottom;
transition: transform 0.3s;
}
.normal-tabs .app-tab-bar ion-tab-button {
transform: scale(1);
transform-origin: bottom;
transition: transform 0.3s;
}
.shrink-tabs .app-tab-bar ion-tab-button {
transform: scale(0.5, 1);
transform-origin: bottom;
transition: transform 0.3s;
}
.shrink-tabs .app-tab-bar .tab-bar ion-label {
opacity: 0;
transition: opacity 0.3s, transform 0.3s;
}
The specific transforms and scales create a nicer shrink and restore animation. To achieve the final result where the header shrinks completely and the tab bar becomes smaller.
Subscribe to my newsletter
Read articles from Teddy Reed directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
