Build a Modern, Customized File Uploading User Interface in React with Plain CSS
Tags: javascript, react, node, webdev
Published: 2019-06-15 23:55:50

Find me on [medium](https://medium.com/@jsmanifest).

Building a user interface around a file input component is a very handy skill to learn as you can go from a 90s look to a more modern finish to compliment your web pages that depend on it — especially when [we can’t style it like any normal HTML element](https://coderwall.com/p/uer3ow/total-input-type-file-style-control-with-pure-css). When users are using your technology, they're not _just_ using it--they're most likely judging your app and its technology as well without you knowing it.

Here's the thing: We can easily show them a file input, let them select files from using the default html element and just have them submit files and call it a day. But _what is happening in between_? What do users like to see when something is happening? An interface that doesn't tell them anything, or an interface that tells them _everything_?

What if the user's internet disconnects? What if the server doesn't respond with anything? What if file 8 of 14 is big for them? What if the user was waiting for the upload process to finish for 10 minutes and wanted to see how far it has gotten from there? Or which files have already been uploaded?

In a previous tutorial (you can find it if you search my posts), I went over building the _logic_ of the getting this api in place. The point of that post was to teach the logic. You can stop there and use it to build your own custom user interface around it. Or you can also build the logic part yourself and read this post for ideas on how to implement UX for any file uploading component. These posts were created for two separate reasons but are perfectly compatible. I'm just going to provide the logic in this post so we can focus on the user interface. The decision is yours :)

While I was coding the user interface, it was getting pretty long that I was contemplating if I should just bring the amount of components down and show a basic UX version. However, a lot of posts these days don't go too far in depth. So i'd like to take this opportunity to have fun and go in more depth to the implementations.

I was deciding whether to use my favorite CSS library _styled-components_ to make this tutorial, however I ended up choosing not to because I wanted to show that a complex user interface can be built without any additional tools. The tools are just a convenience. You just need to learn a little bit of _CSS_, _not_ the tools.

And last but not least, here is a preview of what we will be building in this post:

![complex user interface file uploading component in react with plain css](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/build-complex-custom-file-uploading-ui-in-react-with-plain-css/entire-result.gif)

Without further ado, let's get started!

In this tutorial we are going to quickly generate a react project with [create-react-app](https://github.com/facebook/create-react-app).

Go ahead and create a project using the command below. For this tutorial i’ll call our project _upload-app_.

```bash
npx create-react-app upload-app
```

Now go into the directory once it's done:

```bash
cd upload-app
```

I promised to just provide the logic of the file uploading implementation so we can immediately get started with building the user interface. So here is a custom hook we'll be using, called `useApp.js`:

> src/useApp.js

```jsx
import { useCallback, useEffect, useReducer, useRef } from 'react'

// mock upload func
const api = {
  uploadFile({ timeout = 550 }) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve()
      }, timeout)
    })
  },
}

const logUploadedFile = (num, color = 'green') => {
  const msg = `%cUploaded ${num} files.`
  const style = `color:${color};font-weight:bold;`
  console.log(msg, style)
}

// Constants
const LOADED = 'LOADED'
const INIT = 'INIT'
const PENDING = 'PENDING'
const FILES_UPLOADED = 'FILES_UPLOADED'
const UPLOAD_ERROR = 'UPLOAD_ERROR'
const initialState = {
  files: [],
  pending: [],
  next: null,
  uploading: false,
  uploaded: {},
  status: 'idle',
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'load':
      return { ...state, files: action.files, status: LOADED }
    case 'submit':
      return { ...state, uploading: true, pending: state.files, status: INIT }
    case 'next':
      return {
        ...state,
        next: action.next,
        status: PENDING,
      }
    case 'file-uploaded':
      return {
        ...state,
        next: null,
        pending: action.pending,
        uploaded: {
          ...state.uploaded,
          [action.prev.id]: action.prev.file,
        },
      }
    case 'files-uploaded':
      return { ...state, uploading: false, status: FILES_UPLOADED }
    case 'set-upload-error':
      return { ...state, uploadError: action.error, status: UPLOAD_ERROR }
    default:
      return state
  }
}

const useApp = () => {
  const [state, dispatch] = useReducer(reducer, initialState)
  const onSubmit = useCallback(
    (e) => {
      e.preventDefault()
      if (state.files.length) {
        dispatch({ type: 'submit' })
      } else {
        window.alert("You don't have any files loaded.")
      }
    },
    [state.files.length],
  )

  const onChange = (e) => {
    if (e.target.files.length) {
      const arrFiles = Array.from(e.target.files)
      const files = arrFiles.map((file, index) => {
        const src = window.URL.createObjectURL(file)
        return { file, id: index, src }
      })
      dispatch({ type: 'load', files })
    }
  }

  // Sets the next file when it detects that its ready to go
  useEffect(() => {
    if (state.pending.length && state.next == null) {
      const next = state.pending[0]
      dispatch({ type: 'next', next })
    }
  }, [state.next, state.pending])

  const countRef = useRef(0)

  // Processes the next pending thumbnail when ready
  useEffect(() => {
    if (state.pending.length && state.next) {
      const { next } = state
      api
        .uploadFile(next)
        .then(() => {
          const prev = next
          logUploadedFile(++countRef.current)
          const pending = state.pending.slice(1)
          dispatch({ type: 'file-uploaded', prev, pending })
        })
        .catch((error) => {
          console.error(error)
          dispatch({ type: 'set-upload-error', error })
        })
    }
  }, [state])

  // Ends the upload process
  useEffect(() => {
    if (!state.pending.length && state.uploading) {
      dispatch({ type: 'files-uploaded' })
    }
  }, [state.pending.length, state.uploading])
  return {
    ...state,
    onSubmit,
    onChange,
  }
}

export default useApp
```

## Explanation:

Here is a quick summary of what is going on there:

When users select files, the _onChange_ handler gets invoked. The _e_ argument contains the files we want, accessible by _e.target.files_. These will be the files that will be rendered one by one in the interface. However, this _files_ object isn't an array--it's actually a [FileList](https://developer.mozilla.org/en-US/docs/Web/API/FileList). This is a problem because we can't simply map over this or we'll be given an error. So we convert it to an array and attach it to state.files, allowing the UI to render them row by row in the UI. When the user submits the form, the _onSubmit_ hander gets invoked. It dispatches an action which sends a signal to one or more _useEffects_ that it's time to start. There are several useEffects and each of them are assigned different tasks and conditions. One is used for _starting the flow_, one is used for _continuing the flow_, and one is used to _end the flow_.

What we are going to do next is open up the `App.js` file and replace the default code with:

> src/App.js

```jsx
import React from 'react'
import useApp from './useApp'
import './styles.css'

const Input = (props) => (
  <input
    type="file"
    accept="image/*"
    name="img-loader-input"
    multiple
    {...props}
  />
)

const App = ({ children }) => {
  const {
    files,
    pending,
    next,
    uploading,
    uploaded,
    status,
    onSubmit,
    onChange,
  } = useApp()

  return (
    <form className="form" onSubmit={onSubmit}>
      <div>
        <Input onChange={onChange} />
        <button type="submit">Submit</button>
      </div>
      <div>
        {files.map(({ file, src, id }, index) => (
          <div key={`file-row${index}`}>
            <img src={src} alt="" />
            <div>{file.name}</div>
          </div>
        ))}
      </div>
    </form>
  )
}

export default App
```

And here is our starting CSS file:

> src/styles.css

```CSS
body {
  padding: 12px;
  background: #171c1f;
  color: #fff;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
h1,
h2,
h3,
h4,
h5,
h6 {
  margin: 0;
  font-weight: 500;
}

button {
  transition: all 0.2s ease-out;
  margin: 4px;
  cursor: pointer;
  background: rgb(116, 20, 63);
  border: 0;
  color: #fff;
  min-width: 90px;
  padding: 8px 12px;
  outline: none;
  text-transform: uppercase;
  letter-spacing: 1.3px;
  font-size: 0.6rem;
  border: 1px solid #fff;
}

button:hover {
  background: none;
  color: #fff;
}

button:active {
  background: #fa3402;
}
```

If you run the app, it will look like this:

![1](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/build-complex-custom-file-uploading-ui-in-react-with-plain-css/1.gif)

This is pretty basic. There's really no information to show about these images and UI looks like a page from the 90s.

When you click submit, you can see in the console messages that these are being processed one by one just to be sure:

![2](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/build-complex-custom-file-uploading-ui-in-react-with-plain-css/2.jpg)

But once it finishes you can continue the flow of the app with anything--like redirecting the user to a success page or showing them dog pictures in a modal.

The problem is that the user doesn't know what is going on. They could be waiting for 10 minutes and the page would still stay the same.

So we're going to change this up a bit to have them up to date with _everything_ that is going on from the moment of instantiation to the end of the uploading process.

We'll go ahead and _customize_ the _file input_ so that it looks nicer. We want our users to think we are unique and the best. So we must go above and beyond :)

Currently, our file input looks like this:

![3](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/build-complex-custom-file-uploading-ui-in-react-with-plain-css/3.jpg)

Now since we don't want the user to hit their exit button and never come back, we have to design this further. There are several ways to customize a file input.

This file input component that we are going to make next won't actually be the real input element, but it will disguise itself as the input element by allowing the file browser to be opened when a user clicks on it.

Create a file called `FileUploader.js` and place this code in it:

```jsx
import React from 'react'

const FileUploader = ({ children, triggerInput, inputRef, onChange }) => {
  let hiddenInputStyle = {}
  // If user passes in children, display children and hide input.
  if (children) {
    hiddenInputStyle = {
      position: 'absolute',
      top: '-9999px',
    }
  }

  return (
    <div className="cursor-pointer" onClick={triggerInput}>
      <input
        style={hiddenInputStyle}
        ref={inputRef}
        type="file"
        accept="image/*"
        multiple
        onChange={onChange}
      />
      <div className="uploader">{children}</div>
    </div>
  )
}

export default FileUploader
```

The _real_ file input is the child of the _root div element_ here. The _triggerInput_ will be a function that allows us to tap into the _inputRef_ ref that is attached to the _file input_ element. (We will look at this in the hook in a second).

Now if we render this component and pass in a _children_, the _hiddenInputStyle_ will be applied to the real file input so that it will forcefully show our custom component instead to the UI. _This is how we are overriding the default file input in the interface_.

Inside our hook we defined the _triggerInput_ handler inside:

> src/useApp.js

```jsx
const triggerInput = (e) => {
  e.persist()
  inputRef.current.click()
}
```

Returning it at the end so the caller can access it:

> src/useApp.js

```jsx
return {
  ...state,
  onSubmit,
  onChange,
  triggerInput,
}
```

Great! Now we are going to make the component that will disguise itself as the real file input. It can be anything, but for the sake of this tutorial it will be a mini "_screen_" to the user--guiding them to upload their files and taking them to the next screen by using graphical and textual updates. Since we were rendering _children_ in the render method of _FileUploader_, we can render this screen as a _child of FileUploader_. We want this whole _screen_ to be able to open the file browser when we need it to.

This _screen_ will display text with a background. I'm going to use an image as a background here by creating a folder called `images` in the `src` directory. I'll be placing images used throughout the tutorial here and import images from it.

Make another file called `FileUploaderScreen.js`:

> src/FileUploaderScreen.js

```jsx
import React from 'react'
import idleSrc from './images/jade_input_bg.jpg'

const FileUploaderScreen = (props) => (
  <div className="uploader-input">
    <div
      style={{ backgroundImage: `url("${idleSrc}")` }}
      className="uploader-overlay"
    />
  </div>
)

export default FileUploaderScreen
```

Here are the styles I used for the component:

```css
.form {
  max-width: 400px;
  margin: auto;
}

.uploader {
  display: flex;
  justify-content: center;
  flex-direction: column;
  width: 100%;
  box-sizing: border-box;
}

.uploader-input {
  position: relative;
  transition: all 3s ease-out;
  box-sizing: border-box;
  width: 100%;
  height: 150px;
  border: 1px solid rgb(194, 92, 67);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
}

.uploader-input:hover {
  filter: brightness(100%) contrast(90%);
  border: 1px solid rgb(223, 80, 44);
}

.uploader-input:active {
  filter: brightness(70%);
}

.uploader-input-content {
  color: #fff;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}

.uploader-overlay {
  transition: all 2s ease-out;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: -1;
  background-size: cover;
}

.uploader-overlay:hover {
  filter: brightness(75%);
}

.uploader-overlay:active {
  filter: brightness(40%);
}
.cursor-pointer {
  cursor: pointer;
}
```

Since we're allowed to pass in the imported image as a string to the _backgroundImage_ style property, I used it as the value for the background image.

We mentioned that we want this screen to open up a file browser when clicked so we're going to have to render this inside the FileUploader.

Lets go ahead and put this _FileUploader_ and _FileUploaderScreen_ inside our `App.js` file now:

> src/App.js

```jsx
import React from 'react'
import useApp from './useApp'
import FileUploader from './FileUploader'
import FileUploaderScreen from './FileUploaderScreen'
import './styles.css'

const App = ({ children }) => {
  const inputRef = React.createRef()
  const {
    files,
    pending,
    next,
    uploading,
    uploaded,
    status,
    onSubmit,
    onChange,
    triggerInput,
  } = useApp({ inputRef })

  return (
    <form className="form" onSubmit={onSubmit}>
      <FileUploader
        triggerInput={triggerInput}
        inputRef={inputRef}
        onChange={onChange}
      >
        <FileUploaderScreen />
      </FileUploader>
      <div>
        {files.map(({ file, src, id }, index) => (
          <div key={`file-row${index}`}>
            <img src={src} alt="" />
            <div>{file.name}</div>
          </div>
        ))}
      </div>
    </form>
  )
}

export default App
```

Now when you click the _file upload screen_, you should be able to select files:

![4](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/build-complex-custom-file-uploading-ui-in-react-with-plain-css/4.gif)

Lets make the background image switch to a different one when the user selects files.

**How do we do that?**

This is where we have to use that _status_ state property we defined in our custom hook earlier:

```jsx
const initialState = {
  files: [],
  pending: [],
  next: null,
  uploading: false,
  uploaded: {},
  status: IDLE,
}
```

If you look back at our useEffects and reducer, we made the useEffects dispatch actions depending on what was happening:

> src/useApp.js

```jsx
const reducer = (state, action) => {
  switch (action.type) {
    case 'load':
      return { ...state, files: action.files, status: LOADED }
    case 'submit':
      return { ...state, uploading: true, pending: state.files, status: INIT }
    case 'next':
      return {
        ...state,
        next: action.next,
        status: PENDING,
      }
    case 'file-uploaded':
      return {
        ...state,
        next: null,
        pending: action.pending,
        uploaded: {
          ...state.uploaded,
          [action.prev.id]: action.prev.file,
        },
      }
    case 'files-uploaded':
      return { ...state, uploading: false, status: FILES_UPLOADED }
    case 'set-upload-error':
      return { ...state, uploadError: action.error, status: UPLOAD_ERROR }
    default:
      return state
  }
}
```

> src/useApp.js

```jsx
// Sets the next file when it detects that its ready to go
useEffect(() => {
  if (state.pending.length && state.next == null) {
    const next = state.pending[0]
    dispatch({ type: 'next', next })
  }
}, [state.next, state.pending])

const countRef = useRef(0)

// Processes the next pending thumbnail when ready
useEffect(() => {
  if (state.pending.length && state.next) {
    const { next } = state
    api
      .uploadFile(next)
      .then(() => {
        const prev = next
        logUploadedFile(++countRef.current)
        const pending = state.pending.slice(1)
        dispatch({ type: 'file-uploaded', prev, pending })
      })
      .catch((error) => {
        console.error(error)
        dispatch({ type: 'set-upload-error', error })
      })
  }
}, [state])

// Ends the upload process
useEffect(() => {
  if (!state.pending.length && state.uploading) {
    dispatch({ type: 'files-uploaded' })
  }
}, [state.pending.length, state.uploading])
```

In addition if you look back at the _onChange_ handler, you will see one of these action types being dispatched:

```jsx
const onChange = (e) => {
  if (e.target.files.length) {
    const arrFiles = Array.from(e.target.files)
    const files = arrFiles.map((file, index) => {
      const src = window.URL.createObjectURL(file)
      return { file, id: index, src }
    })
    dispatch({ type: 'load', files })
  }
}
```

Since we know that dispatching _'load'_ will update _state.status_ to _'LOADED'_ we can use that in our _FileUploaderScreen_ to change images whenever _state.status_ updates to '_LOADING_'.

So what we'll do is use a switch case to assign the _src_ to the _backgroundImage_ style property depending on the value of _state.status_:

```jsx
import React from 'react'
import idleSrc from './images/jade_input_bg.jpg'

const FileUploaderScreen = ({ status }) => {
  let src
  switch (status) {
    case 'IDLE':
      src = idleSrc
      break
    default:
      src = idleSrc
      break
  }

  return (
    <div className="uploader-input">
      <div
        style={{ backgroundImage: `url("${src}")` }}
        className="uploader-overlay"
      />
    </div>
  )
}

export default FileUploaderScreen
```

We might as well define some other images to use for other statuses as well:

```jsx
import React from 'react'
import idleSrc from './images/jade_input_bg.jpg'
import pendingSrc from './images/art-arts-and-crafts-bright-1124884.jpg'
import uploadedSrc from './images/adventure-background-blur-891252.jpg'
import errorSrc from './images/121911.jpg'

const FileUploaderScreen = ({ status }) => {
  let src
  switch (status) {
    case 'IDLE':
      src = idleSrc
      break
    case 'LOADED':
    case 'PENDING':
      src = pendingSrc
      break
    case 'FILES_UPLOADED':
      src = uploadedSrc
      break
    case 'UPLOAD_ERROR':
      src = errorSrc
      break
    default:
      src = idleSrc
      break
  }

  return (
    <div className="uploader-input">
      <div
        style={{ backgroundImage: `url("${src}")` }}
        className="uploader-overlay"
      />
    </div>
  )
}

export default FileUploaderScreen
```

Every time the user does something, the image will be different. This is so that we don't bore the user so they're constantly occupied. Do whatever you want to make them stay on your website instead of bouncing away :). Just keep it rated G of course.

Anyways, If you try to select files right now the screen will not update. That is because we need to pass down the _status_ prop to _FileUploaderScreen_:

> src/App.js

```jsx
<FileUploader
  triggerInput={triggerInput}
  inputRef={inputRef}
  onChange={onChange}
>
  <FileUploaderScreen status={status} />
</FileUploader>
```

![5](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/build-complex-custom-file-uploading-ui-in-react-with-plain-css/5.gif)

I don't know about you but I really think those ugly, disproportionate thumbnails need to be tackled next. This isn't the 90s anymore, we have React!

So what we're going to do is we're going to scale them down to fit in _file row_ components (list of rows). In each row, the thumbnail will have a width size of 50px and the height size of 50px. This will ensure that we have enough room on the right to display the file name and file sizes to the user in a clean and professional way.

Create a new file called `FileRow.js` and add this in:

```jsx
import React from 'react'
import Spinner from './Spinner'

const getReadableSizeFromBytes = (bytes) => {
  const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
  let l = 0
  let n = parseInt(bytes, 10) || 0
  while (n >= 1024 && ++l) n /= 1024
  // include a decimal point and a tenths-place digit if presenting
  // less than ten of KB or greater units
  return n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]
}

const Caption = ({ children, label, block, ...rest }) => (
  <div
    style={{ display: block ? 'block' : 'flex', alignItems: 'center' }}
    {...rest}
  >
    <span style={{ color: '#fff' }}>{label}: </span>
    <span style={{ color: '#2b8fba' }}>{children}</span>
  </div>
)

const FileRow = ({ isUploaded, isUploading, file, src, id, index }) => (
  <div
    style={{
      opacity: isUploaded ? 0.2 : 1,
    }}
    className="file-row"
  >
    {isUploading && (
      <Spinner center xs>
        Uploading...
      </Spinner>
    )}
    <div className="file-row-thumbarea">
      <img src={src} alt="" />
      <Caption className="file-row-filename" label="File Name" block>
        {file.name}
      </Caption>
    </div>
    <div className="file-row-additional-info">
      <Caption className="file-row-filesize" label="File Size">
        {getReadableSizeFromBytes(file.size)}
      </Caption>
    </div>
  </div>
)

const isEqual = (currProps, nextProps) => {
  if (currProps.index !== nextProps.index) {
    return false
  }
  if (currProps.isUploaded !== nextProps.isUploaded) {
    return false
  }
  if (currProps.isUploading !== nextProps.isUploading) {
    return false
  }
  return true
}

export default React.memo(FileRow, isEqual)
```

Styles I used:

```css
.file-list {
  font-size: 0.75rem;
}
.file-row {
  position: relative;
  transition: all 0.15s ease-in;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 6px 0;
  max-height: 50px;
  animation: fade 0.6s ease-in;
}

.file-row:hover {
  opacity: 0.7 !important;
}

@keyframes fade {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

.file-row-thumbarea {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-grow: 1;
}

.file-row-thumbarea img {
  transition: all 0.15s ease-out;
  border: 1px solid rgb(170, 26, 110);
  width: 50px;
  height: 50px;
  object-fit: cover;
}

.file-row-filename {
  flex-grow: 1;
  padding: 0 12px;
  font-size: 0.7rem;
}

.file-row-additional-info {
  opacity: 0.7;
}

.file-row-filesize {
  font-style: italic;
  font-size: 0.7rem;
  padding: 3px 6px;
  border-radius: 6px;
  width: 90px;
  text-align: center;
  border: 1px solid rgb(112, 78, 58);
  animation: border-glow 2s ease-in forwards;
}

@keyframes border-glow {
  0% {
    border: 1px solid rgb(94, 68, 54);
  }
  100% {
    border: 1px solid rgb(255, 74, 2);
  }
}
```

Here's what's happening:

1. We defined a _FileRow_ component that will receive the necessary props to render its children components. _file_, _src_, _id_, and _index_ comes from the state.files array set by the _onChange_ handler inside our `useApp` custom hook.
2. _isUploading_'s purpose here is to render an "Uploading..." text and a loading spinner right on top of it when it is being uploaded somewhere.
3. _isUploaded_'s purpose is to shade out rows when their file object is inside _state.uploaded_--mapped by their id. (This was why we had *state.uploaded *if you were wondering)
4. Since we don't want each row to render each time a state is updated, we had to wrap it with a _[React.memo](https://reactjs.org/docs/react-api.html#reactmemo)_ to memoize the props so that they update only when _index_, _isUploading_ or _isUploaded_ changes. While these files are uploading, these props will _never_ change unless something important happened, so it is safe to apply these conditions.
5. _getReadableSizeFromBytes_ was provided so that we render a human readable file size. Otherwise, users will be reading numbers like _83271328_.
6. _Spinner_ is a loading spinner

For the purposes of this tutorial I used [react-md-spinner](https://github.com/tsuyoshiwada/react-md-spinner). Also, I used the [classnames](https://github.com/JedWatson/classnames) package to combine/conditionally render class names for conditional styling for more ease of control.

Note: If you decide to follow through with react-md-spinner/classnames and get this error: 
```bash
Cannot find module babel-preset-react-app/node_modules/@babel/runtime
```

You might need to install `@babel/runtime` (Thanks Morris Warachi)

> src/Spinner.js

```jsx
import React from 'react'
import MDSpinner from 'react-md-spinner'
import cx from 'classnames'

const Spinner = ({
  children,
  containerProps,
  spinnerProps,
  xs,
  sm,
  center,
}) => (
  <div
    className={cx('spinner-container', {
      'flex-center': !!center,
    })}
    {...containerProps}
  >
    <div>
      <div>
        <MDSpinner
          size={xs ? 15 : sm ? 50 : 100}
          borderSize={xs ? 1 : 2}
          {...spinnerProps}
        />
      </div>
      <h4
        className={cx('spinner', {
          'spinner-xs': !!xs,
        })}
      >
        {children}
      </h4>
    </div>
  </div>
)

export default Spinner
```

Styles I used:

```css
.spinner-container {
  position: relative;
  box-sizing: border-box;
  padding: 15px;
  text-align: center;
  display: flex;
  justify-content: center;
}

.spinner {
  color: #fff;
  margin-top: 18px;
}

.spinner-xs {
  margin-top: 4px;
}
.flex-center {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}
```

_Now_ if you try to select files, the interface looks a lot smoother than before:

![6](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/build-complex-custom-file-uploading-ui-in-react-with-plain-css/6.gif)

What we need to do next is to make the screen display textual updates so that users aren't confused about what is happening. Otherwise the _file uploader screen_ is being useless because it's just rotating images right now.

The trick here is to use the very powerful _state.status_ property like we did with the image rotations.

Knowing this, we can make it render custom components on each status update.

Go to the `FileUploaderScreen.js` file and start by conditionally rendering the "_init/idle_" component:

```jsx
import React from 'react'
import idleSrc from './images/jade_input_bg.jpg'
import pendingSrc from './images/art-arts-and-crafts-bright-1124884.jpg'
import uploadedSrc from './images/adventure-background-blur-891252.jpg'
import errorSrc from './images/121911.jpg'

const Init = () => (
  <div style={{ textAlign: 'center' }}>
    <h2>Upload Files</h2>
    <small>Click here to select your files</small>
  </div>
)

const FileUploaderScreen = ({ status }) => {
  let src
  switch (status) {
    case 'IDLE':
      src = idleSrc
      break
    case 'LOADED':
    case 'PENDING':
      src = pendingSrc
      break
    case 'FILES_UPLOADED':
      src = uploadedSrc
      break
    case 'UPLOAD_ERROR':
      src = errorSrc
      break
    default:
      src = idleSrc
      break
  }

  return (
    <div className="uploader-input">
      {status === 'IDLE' && <Init />}
      <div
        style={{ backgroundImage: `url("${src}")` }}
        className="uploader-overlay"
      />
    </div>
  )
}

export default FileUploaderScreen
```

![7](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/build-complex-custom-file-uploading-ui-in-react-with-plain-css/7.jpg)

It seems like our image is a little bright right now. So we're going to define a couple of class styles to update brightnesses depending on which image is rendered:

```css
.brightness100 {
  filter: brightness(100%);
}

.brightness75 {
  filter: brightness(75%);
}

.brightness50 {
  filter: brightness(50%);
}

.opacity05 {
  opacity: 0.25;
}

.grayscale {
  filter: grayscale(100%) brightness(60%);
}
```

> src/FileUploaderScreen.js

```jsx
const FileUploaderScreen = ({ status }) => {
  let src
  switch (status) {
    case 'IDLE':
      src = idleSrc
      break
    case 'LOADED':
    case 'PENDING':
      src = pendingSrc
      break
    case 'FILES_UPLOADED':
      src = uploadedSrc
      break
    case 'UPLOAD_ERROR':
      src = errorSrc
      break
    default:
      src = idleSrc
      break
  }

  return (
    <div className="uploader-input">
      {status === 'IDLE' && <Init />}
      <div
        style={{ backgroundImage: `url("${src}")` }}
        className={cx('uploader-overlay', {
          brightness50: status === 'IDLE',
          brightness100: status === 'LOADED',
          opacity05: status === 'PENDING',
          grayscale: status === 'FILES_UPLOADED',
        })}
      />
    </div>
  )
}
```

It should be easier to see now:

![8](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/build-complex-custom-file-uploading-ui-in-react-with-plain-css/8.jpg)

Using the same concept as we did with the _Init_ component earlier, we can implement the rest of the components the same way:

> src/FileUploaderScreen.js

```js
import React from 'react'
import cx from 'classnames'
import FileUploader from './FileUploader'
import fileUploadBg from './images/jade_input_bg.jpg'
import Spinner from './Spinner'
import artsCrafts from './images/art-arts-and-crafts-bright-1124884.jpg'
import adventureBeginsBg from './images/adventure-background-blur-891252.jpg'
import errorSrc from './images/121911.jpg'

const Init = () => (
  <div style={{ textAlign: 'center' }}>
    <h2>Upload Files</h2>
    <small>Click here to select your files</small>
  </div>
)

const Loaded = ({ total, getFileUploaderProps }) => (
  <div className="loaded">
    <h2>{total} files loaded</h2>
    <div>What would you like to do?</div>
    <div className="loaded-actions">
      <FileUploader {...getFileUploaderProps()}>
        <button type="button">Upload More</button>
      </FileUploader>
      <div>
        <button type="submit">Submit</button>
      </div>
    </div>
  </div>
)

const Pending = ({ files, pending }) => {
  const total = files.length
  const remaining = Math.abs(pending.length - total)
  return (
    <div className="pending">
      <Spinner sm>
        Uploading <span className="text-attention">{remaining}</span> of{' '}
        <span className="text-attention">{total}</span> files
      </Spinner>
    </div>
  )
}

const Success = () => (
  <div className="success-container">
    <div>
      <h2>Congratulations!</h2>
      <small>You uploaded your files. Get some rest.</small>
      <br />
      <small>Look for the arrow!</small>
    </div>
  </div>
)

const Error = ({ uploadError }) => (
  <div>
    <h2 style={{ color: 'red' }}>
      An error occurred!
      <br />
      {uploadError && uploadError.message}
    </h2>
  </div>
)

const FileUploaderScreen = ({
  status,
  files,
  pending,
  uploadError,
  triggerInput,
  getFileUploaderProps,
}) => {
  let src
  switch (status) {
    case 'IDLE':
      src = fileUploadBg
      break
    case 'LOADED':
    case 'PENDING':
      src = artsCrafts
      break
    case 'FILES_UPLOADED':
      src = adventureBeginsBg
      break
    case 'UPLOAD_ERROR':
      src = errorSrc
      break
    default:
      src = fileUploadBg
      break
  }
  return (
    <div className="uploader-input">
      {status === 'IDLE' && <Init />}
      {status === 'LOADED' && (
        <Loaded
          triggerInput={triggerInput}
          getFileUploaderProps={getFileUploaderProps}
          total={files.length}
        />
      )}
      {status === 'PENDING' && <Pending files={files} pending={pending} />}
      {status === 'FILES_UPLOADED' && <Success />}
      {status === 'UPLOAD_ERROR' && <Error uploadError={uploadError} />}
      <div
        style={{ backgroundImage: `url("${src}")` }}
        className={cx('uploader-overlay', {
          brightness50: status === 'IDLE',
          brightness100: status === 'LOADED',
          opacity05: status === 'PENDING',
          grayscale: status === 'FILES_UPLOADED',
        })}
      />
    </div>
  )
}

export default FileUploaderScreen
```

Here are all the styles used for them:

```css
.loaded {
  text-align: center;
}

.loaded h2 {
  margin: 0;
}

.loaded-actions {
  display: flex;
  justify-content: center;
  align-items: center;
}

.pending {
  transition: all 1s ease-in;
}

.pending span.text-attention {
  margin: auto 3px;
}

.success-container {
  padding: 7px;
  color: #fff;
  text-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
}

.success-container h2 {
  margin: 0;
}
```

The _Loaded_ component is rendered when _state.status_'s value is '_LOADED_'. The odd thing here is that the "Upload More" button is being wrapped by the _FileUploader_ that we created in the beginning. "_What is that doing there_?" you might ask.

After the file upload screen gets passed the initial step, we no longer want the entire component to trigger the file browser anymore. I'll go over this a little more very soon.

The _Pending_ component is used to show that uploading is _in process_ so that they _know_ something is happening while they are waiting. This part is very important for our users!

The _Success_ component is displayed immediately after the upload process is done.

And finally, the _Error_ component is displayed when there was an error while uploading. This is to help the user understand what the current situation is without having them find out themselves.

The next thing we are going to do is update `App.js`:

> src/App.js

```jsx
import React from 'react'
import useApp from './useApp'
import FileUploader from './FileUploader'
import FileUploaderScreen from './FileUploaderScreen'
import FileRow from './FileRow'
import './styles.css'

const App = ({ children }) => {
  const inputRef = React.createRef()
  const {
    files,
    pending,
    next,
    uploading,
    uploaded,
    status,
    onSubmit,
    onChange,
    triggerInput,
    getFileUploaderProps,
  } = useApp({ inputRef })

  const initialFileUploaderProps = getFileUploaderProps({
    triggerInput: status === 'IDLE' ? triggerInput : undefined,
    onChange: status === 'IDLE' ? onChange : undefined,
  })

  return (
    <form className="form" onSubmit={onSubmit}>
      <FileUploader {...initialFileUploaderProps}>
        <FileUploaderScreen
          triggerInput={triggerInput}
          getFileUploaderProps={getFileUploaderProps}
          files={files}
          pending={pending}
          status={status}
        />
      </FileUploader>
      <div className={files.length ? 'file-list' : ''}>
        {files.map(({ id, ...rest }, index) => (
          <FileRow
            key={`thumb${index}`}
            isUploaded={!!uploaded[id]}
            isUploading={next && next.id === id}
            id={id}
            {...rest}
          />
        ))}
      </div>
    </form>
  )
}

export default App
```

We added a new function _getFileUploaderProps_ to our _useApp_ hook:

```jsx
const getFileUploaderProps = (opts) => ({
  inputRef,
  triggerInput,
  onChange,
  status: state.status,
  ...opts,
})
```

The reason why we extracted this part out to a separate function is because in the initial _file uploader screen_ we applied the _triggerInput_ and _onChange_ handler directly on the root component in _FileUploader_. After the first screen changes, we don't want the whole file uploader screen component to trigger file browser anymore (since we _did_ provided an _Upload More_ button on the _second_ screen).

That is why we just had this in the _App_ component:

```jsx
const initialFileUploaderProps = getFileUploaderProps({
  triggerInput: status === 'IDLE' ? triggerInput : undefined,
  onChange: status === 'IDLE' ? onChange : undefined,
})
```

And used it to spread its arguments to _FileUploader_:

```jsx
<FileUploader {...initialFileUploaderProps}>
  <FileUploaderScreen
    triggerInput={triggerInput}
    getFileUploaderProps={getFileUploaderProps}
    files={files}
    pending={pending}
    status={status}
  />
</FileUploader>
```

Now, _FileUploader_ will have all 4 arguments passed in like normal but will have _undefined_ values from _props.triggerInput_ and _props.onChange_ for the rest of the screens. In react, _onClick_ handlers _won't_ fire when they are _undefined_. This disables the click handler so we can instead assign the _Upload More_ button to be the new handler for selecting files.

Here's what the app looks like now:

![10](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/build-complex-custom-file-uploading-ui-in-react-with-plain-css/10.gif)

So far so good. But it seems like the loading spinner in the file rows list are awkwardly pushing things to the side when their file is being uploaded.

Did you notice there was a _flex-center_ property applied on the _Spinner_ component?

```jsx
const Spinner = ({
  children,
  containerProps,
  spinnerProps,
  xs,
  sm,
  center,
}) => (
  <div
    className={cx('spinner-container', {
      'flex-center': !!center,
    })}
    {...containerProps}
  >
    <div>
      <div>
        <MDSpinner
          size={xs ? 15 : sm ? 50 : 100}
          borderSize={xs ? 1 : 2}
          {...spinnerProps}
        />
      </div>
      <h4
        className={cx('spinner', {
          'spinner-xs': !!xs,
        })}
      >
        {children}
      </h4>
    </div>
  </div>
)
```

Yes, we're missing the css. So lets slap that right into the css file:

```css
.flex-center {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}
```

![11](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/build-complex-custom-file-uploading-ui-in-react-with-plain-css/entire-result.gif)

## Conclusion

And that concludes the end of this tutorial! If you want to see the bonus part (the green arrow pointing down to the _Next Page_ button, you can see the implementation in the source code at github [here](https://github.com/jsmanifest/upload-app)).

I apologize in advance for the rush towards the end of this tutorial. I wasn't sure if it was getting too long or getting too boring :) Let me know how this tutorial went for you!

Thank you for reading and look forward for more quality posts coming from me in the future!

Follow me on [medium](https://medium.com/@jsmanifest)