Building the Twitter UI with Ionic Components Last update: 2021-01-26

Building the Twitter UI with Ionic Components

What if you could build any popular UI with Ionic components? This tutorial on building a Twitter UI with Ionic is the start to a new series of tutorials!

View Demo

You like to see popular apps built with Ionic? Check out my latest eBook Built with Ionic for even more real world app examples!

Although the default Ionic components look great and can be customised, there’s a lot of uncertainty when it comes to building more advanced UIs or establishing a specific UX.

Within this tutorial we will build the popular Twitter timeline UI including a fading header, sticky segment, scrollable story section and of course the tweet view.

twitter-ui-with-ionic

We’re going to touch a lot of files but we can rely mostly on Ionic components and a bit of additional CSS here and there.

Starting the Twitter UI App

To follow along, simply bring up a new Ionic app without any further plugins. We will actually use the tabs template this time since this will save a bit of time, but you can also build an Ionic tab bar easily yourself.

ionic start devdacticTwitter tabs --type=angular

# A custom component for Tweets
ionic g module components/sharedComponents --flat
ionic g component components/tweet

# Custom directives for manipulating the header
ionic g module directives/sharedDirectives --flat
ionic g directive directives/HideHeader
ionic g directive directives/StickySegment

We can also directly generate a module for components and directives with a few files so we are done with the file setup for the whole tutorial.

I’ve also prepared some dummy data to build our timeline on, therefore we need to inject the HttpClientModule to make a simple GET request later. Go ahead and add it to the app/app.module.ts:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, HttpClientModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Since we are using the tabs template we don’t need any other routing setup. We will also leave out the side menu aspect of the UI and focus only on one page, but adding an Ionic side menu later is no problem as well!

Changing the Ionic Tab Bar UI

Our first real task is changing the tab bar to use some cool Ionicons and on top of that an example of the badge component, which can be used to display a cool notification count.

In fact you could also create a custom div and style it, but we’ll try to rely as much as possible on Ionic components and customise them to our needs!

Go ahead and change the tabs/tabs.page.html to:

<ion-tabs>

  <ion-tab-bar slot="bottom">
    <ion-tab-button tab="tab1">
      <ion-icon name="home-outline"></ion-icon>
    </ion-tab-button>

    <ion-tab-button tab="tab2">
      <ion-icon name="search-outline"></ion-icon>
    </ion-tab-button>

    <ion-tab-button tab="tab3">
      <ion-badge color="primary">11</ion-badge>
      <ion-icon name="notifications-outline"></ion-icon>
    </ion-tab-button>

    <ion-tab-button tab="tab3">
      <ion-icon name="mail-outline"></ion-icon>
    </ion-tab-button>
  </ion-tab-bar>

</ion-tabs>

Right now the circle of the badge doesn’t really look round and Twitter like, so we can reposition it a bit, change the padding and dimensions of it and make sure it looks round and decent.

To do so, simply add the following to the tabs/tabs.page.scss:

ion-badge {
    top: 7px;
    left: calc(50% + 1px);
    font-size: 9px;
    font-weight: 500;
    border-radius: 10px;
    padding: 2px 2px 2px;
    min-width: 18px;
    min-height: 18px;
    line-height: 14px;
}

Cool, step one is done, let’s leave the tabs and get into the timeline.

Creating the Timeline Overview

Now we need to fetch some data so we can build a dynamic, more realistic UI. I’ve hosted a JSOn file I faked with some data over at https://devdactic.fra1.digitaloceanspaces.com/twitter-ui/tweets.json.

We can now load this data into an array of tweets, and while we are here, define some options for the Ionic slides that we will use for the story/fleet section of our view.

These settings will help us to easily display multiple slides on one page so we can scroll through them. By using a value between two numbers you can make sure you see this cut of the next item inside a list, which usually indicates there’s more to scroll for the user!

Now continue with the setup inside the tab1/tab1.page.ts:

import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-tab1',
  templateUrl: 'tab1.page.html',
  styleUrls: ['tab1.page.scss']
})
export class Tab1Page implements OnInit {
  tweets = [];
  segment = 'home';
  opts = {
    slidesPerView: 4.5,
    spaceBetween: 10,
    slidesOffsetBefore: 0
  };

  constructor(private http: HttpClient) {}

  ngOnInit() {
    this.http.get('https://devdactic.fra1.digitaloceanspaces.com/twitter-ui/tweets.json').subscribe((data: any) => {
      console.log('tweets: ', data.tweets);
      this.tweets = data.tweets;
    });
  }
}

We got the data, now we can display it inside our timeline!

But since it’s very likely you reuse a tweet in multiple places inside the app, we directly use the app-tweet component to display a tweet, which is the custom component we generated in the beginning.

There’s also a bit more on this page to discover:

  • The header area and the segment get a template reference which will be used later when we add the fading directive
  • The header contains only some static buttons inside the different available slots
  • The segment is used to switch between different views inside the page. You could also use something like super tabs for swipeable tabs in there.
  • The segment directly set’s the mode of the component to “md”(Material Design) since this fits the UI of Twitter better than the iOS design of the component
  • The story section in our view creates a small avatar image for each tweet, and by passing in our previously created options we can define the UI of the slides from code!

It looks quite plain from the first view, but the power is really in the small details of this page. So go ahead and change the tab1/tab1.page.html to:

<ion-header #header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-button>
        <ion-icon slot="icon-only" name="menu-outline"></ion-icon>
      </ion-button>
    </ion-buttons>
    <ion-title>
      <ion-icon name="logo-twitter" color="primary" size="large"></ion-icon>
    </ion-title>
    <ion-buttons slot="end">
      <ion-button>
        <ion-icon slot="icon-only" name="pulse-outline"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-segment [(ngModel)]="segment" mode="md" #segmentcontrol>
  <ion-segment-button value="home">
    <ion-label>Home</ion-label>
  </ion-segment-button>
  <ion-segment-button value="content">
    <ion-label>cr-content-suggest</ion-label>
  </ion-segment-button>
</ion-segment>

<ion-content>
  <div [hidden]="segment != 'home'">
    <!-- Stoy section at the top -->
    <ion-slides [options]="opts">
      <ion-slide *ngFor="let tweet of tweets">
        <ion-avatar>
          <img [src]="tweet.img">
        </ion-avatar>
      </ion-slide>
    </ion-slides>

    <!-- List of Tweets -->
    <app-tweet *ngFor="let tweet of tweets" [tweet]="tweet"></app-tweet>
  </div>

  <ion-fab vertical="bottom" horizontal="end" slot="fixed">
    <ion-fab-button>
      <ion-icon name="pencil"></ion-icon>
    </ion-fab-button>
  </ion-fab>

  <div [hidden]="segment != 'content'">
    Other content
  </div>
</ion-content>

Now this looks ok, but we’ll run into a few problems with the content scrolling behind the header later, or the segment being covered by the view and not sticky at all.

Therefore, head over to some small CSS optimisations inside the tab1/tab1page.scss:

ion-toolbar {
    --border-style: none;
}

ion-header {
    background: #fff;
}

ion-slides {
    padding-top: 8px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--ion-color-light);
}

ion-segment {
    z-index: 10;
    background: #fff;
}

ion-segment-button {
    text-transform: none;
}

ion-avatar {
    border: 2px solid var(--ion-color-primary);
    padding: 2px;
}

For some components we can directly change the styling, for some Ionic components wee need to use the according CSS variable.

Rule of thumb: Check out the component docs for a specific component and see if a CSS variable is defined and use it, otherwise use plain CSS rules.

Your code isn’t compiling right now? No suprise!

We are using a custom component but we haven’t imported the modules we generated in the beginning. Therefore, quickly head over to the tab1/tab1.module.ts and import them as well:

import { IonicModule } from '@ionic/angular';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Tab1Page } from './tab1.page';
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';

import { Tab1PageRoutingModule } from './tab1-routing.module';
import { SharedComponentsModule } from '../components/shared-components.module';
import { SharedDirectivesModule } from '../directives/shared-directives.module';

@NgModule({
  imports: [
    IonicModule,
    CommonModule,
    FormsModule,
    ExploreContainerComponentModule,
    Tab1PageRoutingModule,
    SharedComponentsModule,
    SharedDirectivesModule
  ],
  declarations: [Tab1Page]
})
export class Tab1PageModule {}

So now we got a first glimpse at the timeline, but the custom component is more like a dummy component right now, so we know what’s next.

Creating a Custom Tweet Component

Within our custom component we got the information of one tweet, so we need to craft a view around that information.

But before, we need to make sure that our module actually exports our component (this might be the reason your code right now is still not working).

Go ahead and add it to the exports inside the components/shared-components.module.ts:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TweetComponent } from './tweet/tweet.component';

@NgModule({
  declarations: [TweetComponent],
  imports: [
    CommonModule
  ],
  exports: [TweetComponent]
})
export class SharedComponentsModule { }

Each tweet has some information and the actual text, and since the hashtags and mentions on Twitter have a different color, we can apply a simply regex to transform these words with a custom class.

That’s the only change we need to apply for the component, so go ahead and change the components/tweet/tweet.component.ts to:

import { Component, Input, OnInit } from '@angular/core';
import { ViewEncapsulation } from '@angular/core'

@Component({
  selector: 'app-tweet',
  templateUrl: './tweet.component.html',
  styleUrls: ['./tweet.component.scss'],
  encapsulation: ViewEncapsulation.None
})
export class TweetComponent implements OnInit {
  @Input() tweet: any;

  constructor() { }

  ngOnInit() {
    console.log('one tweet: ', this.tweet);
    this.parseTweet();
  }

  parseTweet() {
    this.tweet.text = this.tweet.text.replace(/#[a-zA-Z]+/g,"<span class="highlight">$&</span>");
    this.tweet.text = this.tweet.text.replace(/@[a-zA-Z]+/g,"<span class="highlight">$&</span>");
  }

}

In fact this wasn’t the only change, we also changed the ViewEncapsulation inside the component. This is necessary because we are using the tweet text inside the innerHtml which renders HTML directly in Angular, but our own styling wouldn’t be applied otherwise!

The tweet component is now built with standard Ionic components, especially the grid.

You can nest this component however you want, so first we divide the 12 colums space into 2 for the tweet avatar and 10 for the rest of the tweet.

Then we further divide the space inside the actual tweet content to display the name and meta information, the tweet content and perhaps an image, and finally a row with all the action buttons for a tweet to share, like and retweet.

Here we can also use some conditional styling and change the color of the buttons according to the value of the tweet.retweet or tweet.liked value. This is of course a simplification of the real Twitter API data but shows the general concept!

Go ahead and create the tweet view inside the components/tweet/tweet.component.html now:

<ion-row class="wrapper">
  <ion-col size="2">
    <ion-avatar>
      <ion-img [src]="tweet.img"></ion-img>
    </ion-avatar>
  </ion-col>
  <ion-col size="10">
    <ion-row class="tweet-info">
      <ion-col size="12">
        <span class="name">{{ tweet.username }}</span>
        <span class="handle">@{{ tweet.handle }}</span>
        <span class="handle">- {{ tweet.date*1000 | date: 'shortDate' }}</span>
      </ion-col>
    </ion-row>
    <ion-row>
      <ion-col size="12">
        <div [innerHtml]="tweet.text"></div>
        <img class="preview-img" [src]="tweet.attachment" *ngIf="tweet.attachment">
      </ion-col>
    </ion-row>
    <ion-row class="ion-justify-content-start">
      <ion-col>
        <ion-button fill="clear" color="medium" size="small">
          <ion-icon name="chatbubble-outline" slot="start"></ion-icon>
          {{ tweet.response }}
        </ion-button>
      </ion-col>
      <ion-col>
        <ion-button (click)="tweet.retweet = !tweet.retweet" fill="clear" [color]="tweet.retweet ? 'primary' : 'medium'" size="small">
          <ion-icon name="repeat-outline" slot="start"></ion-icon>
          {{ tweet.retweets }}
        </ion-button>
      </ion-col>
      <ion-col>
        <ion-button (click)="tweet.liked = !tweet.liked" fill="clear" [color]="tweet.liked ? 'primary' : 'medium'" size="small">
          <ion-icon name="heart-outline" slot="start"></ion-icon>
          {{ tweet.like }}
        </ion-button>
      </ion-col>
      <ion-col>
        <ion-button fill="clear" color="medium" size="small">
          <ion-icon name="share-outline" slot="start"></ion-icon>
        </ion-button>
      </ion-col>
    </ion-row>
  </ion-col>
</ion-row>

Finally again some styling to make everything look even more Twitter like, and to stay concise with the Ionic colors we can directly retrieve the Ionic color theme variables by using the var() function.

Wrap up the component by changing the components/tweet/tweet.component.scss to:

.tweet-info {
    font-size: 0.9em;
}

.name {
    font-weight: 600;
}

.handle {
    padding-left: 4px;
    color: var(--ion-color-medium);
}

.wrapper {
    border-bottom: 1px solid var(--ion-color-light);
}

.highlight {
    color: var(--ion-color-primary);
}

.preview-img {
    border: 1px solid var(--ion-color-light);
    border-radius: 10px;
}

Now we got the whole UI for our view done, but there’s one important UX element missing.

Fly out the Header

If you’ve used the Twitter app you might have noticed that the header fades out on scroll, and the segments are sticky at the top.

To implement a behaviour like this, we can create a custom directive that listens to the scroll events of our content and changes some parts of the DOM.

Just like with our custom component, we now need to export the directives accordingly inside the module we created in the beginning, so go ahead and change the directives/shared-directives.module.ts to:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HideHeaderDirective } from './hide-header.directive';
import { StickySegmentDirective } from './sticky-segment.directive';

@NgModule({
  declarations: [HideHeaderDirective, StickySegmentDirective],
  imports: [
    CommonModule
  ],
  exports: [HideHeaderDirective, StickySegmentDirective]
})
export class SharedDirectivesModule { }

First of all we will take care of the fading header, but to understand the values we are working with better, we can quickly go back to our tab1/tab1.page.html and change the opening content tag to:

<ion-content [fullscreen]="true" scrollEvents="true" [appHideHeader]="header" [appStickySegment]="segmentcontrol">

What this does?

  • Make the content scroll behind the header area by using fullscreen, otherwise it would stop earlier behind a “white box”
  • Emit scroll events from ion-content, which are usually disabled
  • Pass the header template reference to the appHideHeader directive
  • Pass the segmentcontrol template reference to the appStickySegment directive

With this knowledge, we can now build the fading header directive which should move up the header area on scroll and at the same time, fade out the elements.

You could actually also fade out the whole header, but in our case this would result in a strange UI on iOS devices inside the top notch area. We better keep the general header visible and just grab the children of it to change their opacity later.

Basically whenever we receive a scroll event (from ion-content, which is the host of this directive), we grab the current scrollTop value and in combination with the height of the header (which is different for iOS and Android) we calculate the new position to smoothly move the header out.

At the same time, we update the opacity of all the children inside the header and perform this operation using the Ionic DomController which inserts the write operation at the best time for the browser without messing up the view repainting cycle!

Therefore change our first directive inside directives/hide-header.directive.ts to:

import { Directive, Input, HostListener, Renderer2, AfterViewInit } from '@angular/core';
import { DomController, isPlatform } from '@ionic/angular';

@Directive({
  selector: '[appHideHeader]'
})
export class HideHeaderDirective implements AfterViewInit {
  @Input('appHideHeader') header: any;
  private headerHeight = isPlatform('ios') ? 44 : 56;
  private children: any;

  constructor(
    private renderer: Renderer2,
    private domCtrl: DomController,
  ) { }

  ngAfterViewInit(): void {
    this.header = this.header.el;
    this.children = this.header.children;
  }

  @HostListener('ionScroll', ['$event']) onContentScroll($event: any) {
    const scrollTop: number = $event.detail.scrollTop;
    let newPosition = -scrollTop;

    if (newPosition < -this.headerHeight) {
      newPosition = -this.headerHeight;
    }
    let newOpacity = 1 - (newPosition / -this.headerHeight);

    this.domCtrl.write(() => {
      this.renderer.setStyle(this.header, 'top', newPosition + 'px');
      for (let c of this.children) {
        this.renderer.setStyle(c, 'opacity', newOpacity);
      }
    });
  }
}

The second directive follows basically the same setup, on scroll we want to move the segment up. But this time we want to stop the repositioning once we have scrolled up the headerHeight amount and then keep it there, so it becomes sticky at the top.

Remember that we added a z-index for the styling of the segment before, therefore it will stay above all the other content!

Now wrap up the last part by changing the directives/sticky-segment.directive.ts to:

import { Directive, HostListener, Input, Renderer2 } from '@angular/core';
import { DomController, isPlatform } from '@ionic/angular';

@Directive({
  selector: '[appStickySegment]'
})
export class StickySegmentDirective {
  @Input('appStickySegment') segment: any;
  private headerHeight = isPlatform('ios') ? 44 : 56;

  constructor(
    private renderer: Renderer2,
    private domCtrl: DomController
  ) { }

  ngAfterViewInit(): void {
    this.segment = this.segment.el;
  }

  @HostListener('ionScroll', ['$event']) onContentScroll($event: any) {
    const scrollTop: number = $event.detail.scrollTop;
    let newPosition = -scrollTop;

    if (newPosition < -this.headerHeight) {
      newPosition = -this.headerHeight;
    }

    this.domCtrl.write(() => {
      this.renderer.setStyle(this.segment, 'top', newPosition + 'px');
    });
  }
}

And BOOM there we go - the whole Twitter UI with Ionic is ready!

Conclusion

We’ve built a truly flexible Twitter UI and UX with Ionic, which makes use almost completely of Ionic components and some additional styling.

This tutorial is the first in a series of “Built with Ionic” UI tutorials so if you want to see more of these, leave a comment with some UI that I should replicate with Ionic!

You can also find a video version of this tutorial below.