Web Development
Javascript Tutorials
Just Another Ghost  @jst
Writer at Learning Curve and generally just another ghost! Born of a earth and dust to create. Sooner or later everything we make will be destroyed.

How To Make PlayStation 5 UI In Vanilla JavaScript

Learn CSS Visually! Every single CSS property visualized in this pictorial CSS guide book!

best css book for beginner web developers

⭐⭐⭐⭐ and 1/2⭐ - owned by over 27.1K readers.

Get this CSS book in PDF format, on Amazon or start reading this css book on your Kindle device today!

Live PS5.js Demo

Here's the PS5 UI demo made with JavaScript and CSS animations that we're going to create in this tutorial.

Hey devs! Check out this #octopack coding book bundle to help Semicolon authors make more free tutorials for the web dev community. Making demos like this can take weeks! We appreciate the support.

Use Left, Right, Up, Down and Space or Enter keys to navigate.

Click on the PS button to start!

Star or fork ps5.js 35.9 KB @ this GitHub

I tweeted the PS3 demo here when I created basic version of PS3 UI in JavaScript. I don't have the code for it (yet.) But I plan on publishing it. Moreover, this tutorial is built on what I learned from that first work.

The Initial Setup

To make things simple, no frameworks were used.

But even if you use frameworks or libraries, you still need to develop your problem solving pattern. This UI tutorial exposes the thinking behind the process. It can be easily adapted to React, Vue or Angular.

I used this starter HTML file with some pre-made flex styles. It contains all the basics and general app structure to get started. It's not React or Vue, but it's the minimum setup needed to start building on top of. I use it every time I need to start a new vanilla app or site.


Some of the basics from HTML starter file will be explained here.

Simple Home Brew CSS Framework

I'm not a huge fan of CSS frameworks and prefer the pure approach. However, after thousands of hours coding, you start to see patterns that repeat the most. Why not create a few simple classes to cover most common cases? We'll avoid typing and retyping the same property names and values hundreds of times.

.rel { position: relative }
.abs { position: absolute }

.top { top: 0 }
.left { left: 0 }
.right { right: 0 }
.bottom { bottom: 0 }

/ flex /
.f { display: flex; }
.v { align-items: center }
.vs { align-items: flex-start }
.ve { align-items: flex-end }
.h { justify-content: center }
.hs { justify-content: flex-start }
.he { justify-content: flex-end }
.r { flex-direction: row }
.rr { flex-direction: row-reverse }
.c { flex-direction: column }
.cr { flex-direction: column-reverse }
.s { justify-content: space-around }

.zero-padding { padding: 0 }

.o { padding: 5px }
.p { padding: 10px }
.pp { padding: 20px }
.ppp { padding: 30px }
.pppp { padding: 50px }
.ppppp { padding: 100px }

.m { margin: 5px }
.mm { margin: 10px }
.mmm { margin: 20px }
.mmmm { margin: 30px }

These CSS classes are self-explanatory.

Our first custom CSS styles

Now that we have basic CSS defined, let's add some styles that modify appearance of hidden and shown menu containers. Remember, because we have multiple menus and can switch between them, we need a way to indicate which menus are currently "on" and which are turned "off".

By multiple menus, I mean each menu will have its own screen, defined by a separate HTML element. Switching to a next menu will hide previous container and display the new one. CSS transitions modifying opacity, position and scale might also play during this process to create smooth UX transitions.

All containers with .menu class will be in "off" state by default (hidden from view.) Any element with classes .menu and .current will be considered in "on" state and be visible on the screen.

Other items such as selectable buttons within the menus themselves will also use .current class but in a different CSS hierarchy context. We'll explore their CSS styles further down in this tutorial.

#ps5 {
   width: 1065px;
   height: 600px;
   background: url('');
   background-size: cover;

/ default menu container - can be any UI screen /
#ps5 {
    display: none;
    opacity: 0;

    // gives us automatic transitions between opacities
    // which will create fade in/fade out effect.
    // without writing any additional JavaScript
    transition: 400ms;      

#ps5 {
    display: flex;
    opacity: 1;

Again is the default parent container for any of the menu layers we will create. It can be the "games browser" screen or "settings" screen. It's invisible by default until we apply .current class to element's classlist property.

And indicates the currently selected menu. All other menus should be invisible and .current class should never be applied to more than one menu at a time!


Our home brew mini CSS framework makes HTML pretty straightforward. Here is the main scaffold:

    <section id = "ps5" class = "rel">
        <section id = "system" class = "menu f v h"></section>
        <section id = "main" class = "menu f v h"></section>
        <section id = "browser" class = "menu f v h"></section>
        <section id = "settings" class = "menu f v h"></section>

The ps5 element is the main app container.

Most of flex is f v h for centered elements so you'll see this combo often.

You'll also see f r for flex-direction:row; and f c for flex-direction:column;

Subsections are separate menu views that require class menu. We'll be able to switch between them.

In code, they will be enumerated with a frozen object (you'll see this below.)

Background Swapper

One of the very first things I wanted to get out of the way was the background changer. If I could get it done first, I could simply integrate it later into all future functions that needed to change the background. To do this I decided to create 2 interchangeable divs.

Whenever a new background becomes active, I simply swap the two divs, change style.background property to the new image URL and applly .fade-in class to the new background, removing it from the previous one.

I started with the following CSS:

#background-1, #background-2 {
    position: absolute;
    top: 0;
    left: 0;
    width: inherit;
    height: inherit;
    background: transparent;
    background-position: center center;
    background-size: cover;
    pointer-events: none;
    transition: 300ms;
    z-index: 0;
    opacity: 0;
    transform: scale(0.9)

/ This class will be applied from Background.change() function /
.fade-in { opacity: 1 !important; transform: scale(1.0) !important; z-index: 1 }

/ set first visible background /
#background-2 { background-image: url(; }

Then I created helper static function .change stemming from Background class that basically swaps the two divs and faded them in and out (It takes one argument - the URL of the next image):

class Background {constructor() {}}

Background.change = url => {

    console.log(Changing background to

    let currentBackground = $(.currentBackground);
    let nextBackground = $(.nextBackground);

    // set new background to url = url(

    // fade in and out

    // swap background identity

From now on, every time I needed to show a new background, I would simply call this function with the image of the URL I wanted to show up as follows:


Fade ins will be taken care automatically because transform: 300ms is already applied to each background and the .fade-in class will take care of the rest.

Building Main Navigation Menu

Now that we have our basic scaffold ready, we can start building out the rest of the UI. But we'll also need to create a class to manage our UI. We'll call this class PS5Menu. How to use it will also be explained below.

The System Screen

Simple CSS was used to create the Start button. Once user clicks, we enter the main PS5 menu. We'll place this Start button into our first menu on the screen - the system menu:

<section id = "system" class = "menu f v h">
    <div id = "start" class = "f v h">Start</div>

Likewise, content for all other menus will be placed within their respective parent container elements.

We'll get there later in this tutorial. First, we need to think about how to organize multiple menu screens.

At this point we need to know about the concept of queuing multiple menus. PS5 has several layers of different navigation UIs. When choosing Settings, for example, a new, completely different menu opens up and keyboard controls are transferred to that new menu.

We need something to keep track of all these menus constantly opening up, being cancelled and then being replaced by a new or a previous menu again.

You can use JavaScript Array's built-in push method to add new menu to the queue. And when you need to go back you can call Array's pop method to go back to whatever the previous menu was.

We will enumerate menus by element's id attribute:

const MENU = Object.freeze({
    system: system,
      main: main,
   browser: browser,
  settings: settings,

/ add more if needed/


I used Object.freeze() to make sure none of the properties can be changed once they are set. Some types of objects are best frozen. They are the ones you 100% know shouldn't change during lifetime of your application.

Here each value is property name in string format. This way we can refer to menus' elements by MENU.system or MENU.settings from now on. This is nothing more than syntactic aesthetics, if not a simple way to keep all menu objects in the same basket, so to speak.

The PS5Menu class

To start out, I created PS5Menu class. Its constructor harbors a this.queue property of type Array.

// menu queue object for layered PS5 navigation
class PS5Menu {

    constructor() {
        this.queue = []

    set push(elementId) {
        // hide previous menu on the queue by removing "current" class
        this.queue.length > 0 && this.queue[this.queue.length - 1].classList.remove(current)

        // get menu container
        const menu = $(#${elementId}) 

        // make the new menu appear by applying "current" class
        !menu.classList.contains(current) && menu.classList.add(current)
        // push this element onto the menu queue
        this.queue.push( menu ) 

        console.log(Pushed #${elementId} onto the menu queue)

    pop() {
        // remove current menu from queue
        const element = this.queue.pop()

        console.log(Removed #${element.getAttribute('id')} from the menu queue)

How to use the PS5Menu class?

This class has two methods. The setter push(argument), and static function pop(). They'll do pretty much what array's .push() and .pop methods do on our this.queue array.

For example, to instantiate the menu class and add or remove menu to its stack, we can call push and pop methods directly from the instance of the class.

// instantiate the menu object from class

const menu = new PS5Menu()

// add menu to the stack menu.push = system // remove the last menu that was pushed onto the stack from it


Class setter functions like set push() cannot be called with (). They are assigned a value with = value assignment operator. The class setter function set push() will then execute with that parameter.

Let's put together everything we have so far:

/ Your DOM just loaded /
window.addEventListener('DOMContentLoaded', event => {      

    // Instantiate the queable menu
    const menu = new PS5Menu()

    // Push system menu onto the menu
    menu.push = system

    // Attach click event to Start button
    menu.queue[0].addEventListener(click, event => {

        console.log(Start button pressed!)

        // begin the ps5 demo!
        menu.push = main


Here we instantiated our PS5Menu class and stored its object instance in menu variable.

Then we pushed our first menu with id #system onto our multiple menu queue.

We then attached click event to the Start button. When user will press it, we push the main menu (whose id is main) to be our current menu. This will hide the system menu (the menu currently in our menu queue,) and will show the #menu container.

Note, because our menu container class .menu.current has transform: 400ms; property, simply adding or removing .current class from the element newly added or removed properties will be animated in 0.4 milliseconds.

Now we need to think about how to create content for the main menu.

Note this step is taken in DOM's "Content Loaded" event DOMContentLoaded. This should be the entry point to any UI-based app. The second entry point is window.onload but we don't need it in this demo. It's for waiting when media (images, etc.) is finished downloading, which can take place much later after DOM elements are available.

The Intro Screen

The main UI starts out as a row of several elements. The entire row emerges from the right side of the screen. It is animated left, as the row shows up for the first time.

I embedded these items into #main container as follows:

<section id = "main" class = "menu f v h">
    <section id = "tab" class = "f">
        <div class = "on">Games</div>
    <section id = "primary" class = "f">
        <div class = "sel t"></div>
        <div class = "sel b current"></div>
        <div class = "sel a"></div>
        <div class = "sel s"></div>
        <div class = "sel d"></div>
        <div class = "sel e"></div>
        <div class = "sel"></div>
        <div class = "sel"></div>
        <div class = "sel"></div>
        <div class = "sel"></div>
        <div class = "sel"></div>

The first PS5 menu is placed within parent container whose style is defined as follows:

#primary {
    position: absolute;
    top: 72px;
    left: 1200px;
    width: 1000px;
    height: 64px;
    opacity: 0;

    / animate at the rate of 0.4s /
    transition: 400ms;

#primary.hidden {
    left: 1200px;

By default, in its "hidden" state #primary is intentionally not shown, and moved far enough right by 1200px.

You just have to use trial and error and some of your intuition. A value of 1200px seemed to work well. This container also inherits opacity:0 from .menu class.

So when #primary appears for the first time it looks like it's sliding and fading in at the same time.

The value transform:400ms; (same as 0.4s) was used here again, because most micro animations look good at 0.4s, for some reason. 0.3s is still good, but might be too fast and 0.5s too slow.

Using CSS Transitions To Control UI Animations

Instead of manipulating CSS styles manually every time we need to change the style or position of a UI block, we can simply assign and remove classes:

// get element:

const element = $(#primary)

// check if element already contains a CSS class:"menu")

// add a new class to element's class list:"menu")

// remove a class from element's class list:"menu")

This is an important strategy that will save a lot of time and keep your code cleaner in any vanilla project. Instead of changing property style.left we will simply remove .hidden class from #primary element. Because it has transform:400ms; the animation will play automatically.

We'll use this tactic to change pretty much every state of any UI element.

Secondary Slide Out Animation

There are different types of animation when dealing with UX design. Some animations are triggered by switching to a new menu. They usually start after a short span of time, soon after switching to a new screen.

There are also on cursor hover animations that are triggered by mouse or controler when selecting a new adjacent item on the current navigation menu, for example.

Attention to detail is important. Especially when you're looking to build a high-quality product.

Using setTimeout Function To Manage Animation States

Once the items slide out a secondary subtle animation plays. JavaScript's setTimeout function was used to imitate this two-fold effect, soon as DOM tree is fully loaded.

Because this is the first menu screen that appears soon as user clicks Start button, we now need to to update our Start button's click event in our DOMContentLoaded event right after menu.push = main.

The following code will go to the bottom of our already-existing DOMContentLoaded (see source code example above for initial setup) event function:

/ Your DOM just loaded /
window.addEventListener('DOMContentLoaded', event => {      

    / Initial setup code goes here...see previous source code example /

    // Attach click event to Start button
    menu.queue[0].addEventListener(click, event => {

        console.log(Start button pressed!)

        // begin the ps5 demo!
        menu.push = main

        // new code: animate the main UI screen for the first time
        // animate #primary UI block within #main container

        // animate items up
        let T1 = setTimeout(nothing => {


            // destroy this timer
            T1 = null;

        }, 500)


So Far We Have This

All this code we wrote will result in this initial animation:

Building Selectable Items

We already created the CSS for the selectable items (.sel class.)

But this still looks quite plain and not exactly as shiny as the PS5 experience.

The following section will explore how we can make the experience more glossy. We'll bring this part of the UI to that professional look of the PlayStation 5 navigation system.

Default "Selected" or "Current" Item Animation

Three Types Of Animations On Currently Selected Item

Currently selected items in the PS5 UI have 3 visual effects. Rotating "halo" outline, occasional lightspot moves across the background, and finally the "lightwave", an effect that looks like a wave is moving in the direction of the arrow button that was pressed on the controller.

In this section we'll take a look at how to create the classic PS5 "shiny" button outline effect, the background lightspot and the lightwave effect as well. Below is the break down of each animation type and CSS classes we need for all of those types:

Animated Gradient Halo

This effect adds an animated border that rotates around selected item.

In CSS, it can be imitated by a rotating conic gradient.

Left: Animated conic gradient. Right: Same element with an icon overlay.

First, here is our generic selectable item CSS setup:

.sel {
    position: relative;
    width: 64px;
    height: 64px;
    margin: 5px;
    border: 2px solid #1f1f1f;
    border-radius: 8px;
    cursor: pointer;
    transition: 400ms;
    transform-style: preserve-3d;
    z-index: 3;

.sel.current {
    width: 100px;
    height: 100px;    

.sel .under {
    position: absolute;
    width: calc(100% + 8px);
    height: calc(100% + 8px);
    margin: -4px -4px;
    background: #1f1f1f;
    transform: translateZ(-2px);
    border-radius: 8px;
    z-index: 1;

.sel .lightwave-container {
    position: relative;
    width: 100%;
    height: 100%;
    transition: 400ms;
    background: black;
    transform: translateZ(-1px);
    z-index: 2;
    overflow: hidden;

.sel .lightwave {
    position: absolute;
    top: 0;
    right: 0;
    width: 500%;
    height: 500%;    
    background: radial-gradient(circle at 10% 10%, rgba(72,72,72,1) 0%, rgba(0,0,0,1) 100%);
    filter: blur(30px);
    transform: translateZ(-1px);
    z-index: 2;
    overflow: hidden;

I tried using ::after and ::before pseudo elements, but wasn't able to easily achieve results I wanted, browser support is questionable and there is no native way to access pseudo elements in JavaScript.

structure of PS5 selectable UI button in HTML, CSS and JavaScript

Instead I decided to create a new element .under and decreased its position on Z-axis by -1, by using transform: translateZ(-1px) which pushes it away from the camera view, and letting its parent show up in front of it.

You might also need to include transform-style: preserve-3d; property on parent elements identified by .sel as well in order to enable z-order in element's 3D space.

Ideally, you'd want to make your .under layer the parent of the item and build lightspot and the actual button element inside it. But translateZ trick overrides all that, and it's just how I already started building the UI. It can be reworked, but at this point there isn't a necessity.

The HTML is straightforward. The important thing here is that now we have a new element .under. This is the element on which rotating conic gradient will be rendered to create the thin shiny border.

The .lightwave-container will help us cut out the light roll effect by using overflow: hidden. The .lightwave is the element on which the actual effect will be rendered, which is a larger div that goes outside the button's boundaries and contains a displaced radial gradient.

<div id = "o0" data-id = "0" class = "sel b">
    <div class = "under"></div>
    <div class = "lightwave-container">
        <div class = "lightwave"></div>

As of 02-10-2021 CSS animation does not support background gradient rotation.

To circumvent that, I used JavaScript's built-in window.requestAnimationFrame function. It will smoothly animate your background property matching your monitor's frame rate, which is usually 60FPS:

// Continuously rotate currently selected item's gradient border
let rotate = () => {

    let currentlySelectedItem = $(.sel.current .under)
    let lightwave = $(.sel.current .lightwave)

    if (currentlySelectedItem) {

        let deg = parseInt(selectedGradientDegree);
        let colors = #aaaaaa, black, #aaaaaa, black, #aaaaaa;

        // dynamically construct the css style property
        let val = conic-gradient(from ${deg}deg at 50% 50%, ${colors});

        // rotate the border = val

        // rotate lightwave = rotate(${selectedGradientDegree}deg);

        // rotate the angle
        selectedGradientDegree += 0.8

This is the function responsible for animating the rotating border and the larger lightwave element too.

Background Lightspot

Occassionally, every 2-3 seconds or so, the background of a currently selected item will play a lightspot animation. It's a subtle blob of light that travels across the element. I used a rotating transparent conic gradient to recreate this effect.

Here's what we have so far. Notice the secondary "moving lightspot" background effect spining at a slightly slower rate (because it's larger than the border gradient.)

We just put together a two-fold animation that makes our button look quite good.

It's not exactly the same as PS5 buttons, but that doesn't make it any worse. It's close enough and you can always continue tweaking it if you want something different.

Next we'll create controller-triggered lightwave effects.

Write For Us
Sign Up Now  -  It's Free!
© 2021
DM Coming Soon
f f