<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#ffffff">
    <title>React App</title>
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
  </body>
</html>
* {
  box-sizing: border-box;
}

body {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  min-height: 100vh;
  margin: 0;
  padding: 0;
  font-family: sans-serif;
  overflow-x: hidden;
  overflow-y: auto;
}


@keyframes letterLoaderTop {
    0% {
        transform: scale(1, 1) rotate(45deg) skew(0deg, 0deg) translate(35%, 35%);
        z-index: -1;
        opacity: 0;
    }

    40% {
        transform: scale(1, 1) rotate(45deg) skew(0deg, 0deg) translate(0, 0);
        z-index: -1;
        opacity: 1;
    }

    60% {
        transform: scale(1, 1) rotate(45deg) skew(0deg, 0deg) translate(0, 0);
        z-index: 1;
        border-color: transparent;
        opacity: 1;
    }

    100% {
        transform: scale(1, -1) rotate(45deg) skew(0deg, 0deg) translate(-1%, -1%);
        z-index: 1;
        border-color: hsl(210, 50%, 95%);
        opacity: 1;
    }
}

@keyframes spin {
    0% {
        transform: rotate(0deg);
    }

    100% {
        transform: rotate(360deg);
    }
}

@keyframes swoosh {
    0% {
        transform: translateX(0);
    }

    30% {
        transform: translateX(-20%);
    }

    100% {
        transform: translateX(150vw);
    }
}

.form {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    max-width: 90vw;
    padding: 40px;
    border: 2px solid hsl(210, 50%, 90%);
    background-color: hsl(210, 50%, 95%);
}

.form__loader {
    pointer-events: none;
}

.form__loader::before {
    content: "";
    position: absolute;
    top: -32vw;
    left: 15%;
    width: 70%;
    padding-top: 70%;
    border: 3px solid transparent;
    border-bottom: 0;
    border-right: 0;
    transform: rotate(45deg) translate(35%, 35%);
    background: linear-gradient(135deg, hsl(210, 50%, 90%) 0%, hsl(210, 50%, 90%) 50%, transparent 50.1%, transparent 100%);
    z-index: -1;
    opacity: 0;
}

.form__loader::after {
    content: "";
    box-sizing: border-box;
    position: absolute;
    top: 35vw;
    left: calc(50% - 50px);
    width: 100px;
    height: 100px;
    border-radius: 100%;
    border: 10px solid hsl(210, 50%, 85%);
    border-top-color: hsl(210, 50%, 75%);
    z-index: 1;
    animation: spin 1.4s infinite linear;
    opacity: 0;
    transition: opacity 0.4s ease-out;
}

.form.is--loading,
.form.is--sent {
    cursor: not-allowed;
}

.form * {
    transition: opacity 0.4s ease-out;
}

.form.is--loading *:not(.form__loader),
.form.is--sent *:not(.form__loader) {
    transition-delay: 0.6s;
    opacity: 0;
    pointer-events: none;
}

.form.is--loading .form__loader::before {
    animation: letterLoaderTop 1s forwards;
}

.form.is--loading .form__loader::after {
    opacity: 1;
    transition-delay: 1s;
}

.form.is--sent {
    animation: swoosh 1s forwards;
}

.form.is--sent .form__loader::before {
    animation: letterLoaderTop 1s forwards paused;
    animation-delay: -0.9s;
}

.form.is--sent .form__loader::after {
    opacity: 0;
}

@media screen and (min-width: 768px) {
    .form {
        max-width: 690px;
    }

    .form__loader::before {
        top: -59%;
    }

    .form__loader::after {
        top: 65%;
    }
}


.button {
    display: inline-block;
    margin: auto;
}

.button__element {
    padding: 0.5em 1ch;
    border: none;
    font-size: 14px;
    text-transform: uppercase;
    color: hsl(210, 50%, 20%);
    background: hsl(210, 50%, 90%);
    cursor: pointer;
    box-shadow: 0 0 0 2px hsl(210, 50%, 20%);
}

.button__element:hover,
.button__element:focus {
    color: hsl(210, 50%, 90%);
    background: hsl(210, 50%, 20%);
    box-shadow: 0 0 0 2px hsl(210, 50%, 90%);
}



.input {
    position: relative;
    margin-bottom: 2em;
    display: flex;
    flex-wrap: wrap;
    flex: 1 0 100%;
}

.input::before,
.input::after {
    content: "";
    position: absolute;
    bottom: 0;
    width: 100%;
    height: 2px;
    background-color: hsl(210, 50%, 20%);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform 200ms ease-in-out;
}

.input::before {
    background-color: hsl(210, 50%, 90%);
    transform: scaleX(1);
}

.input:focus-within::after {
    transform: scaleX(1);
}

.input__label {
    display: block;
    margin-bottom: 2px;
    font-size: 12px;
    text-transform: uppercase;
    color: hsl(210, 50%, 20%);
}

.input__field {
    width: 100%;
    padding: 0.2em 0.5ch;
    border: none;
    font-size: 16px;
    background-color: hsl(210, 50%, 90%);
}

.input__field.is--large {
    min-height: 10em;
}

.input__field:focus {
    outline: none;
}

@media screen and (min-width: 768px) {
    .input {
        flex-wrap: nowrap;
    }

    .input__label {
        flex: 1 1 20%;
        min-width: 140px;
        margin-top: 0.5em;
        text-align: right;
        padding-right: 1ch;
    }

    .input__field {
        flex: 1 1 80%;
    }
}
class App extends React.Component {
    constructor(props) {
        super(props);

        this.opts = {
            'formStateDefault': null,
            'formStateLoading': 'loading',
            'formStateSent': 'sent'
        };

        this.state = {
            formState: this.opts.formStateDefault
        };

        this.el = React.createRef();
        this.nameInput = React.createRef();
        this.mailInput = React.createRef();
        this.messageInput = React.createRef();
    }

    /**
     * triggers the loading animation and serializes the form
     * prevents the submit event
     *
     * @param e
     */
    evaluateForm = (e) => {
        e.preventDefault();

        this.setState({
            formState: this.opts.formStateLoading
        });

        let data = new FormData(this.el.current);
        this.sendForm(data);
    };

    /**
     * Mockup function to send the data to a hypothetic API endpoint
     *
     * @param data
     */
    sendForm = (data) => {
        // mockup: send data here
        window.setTimeout(() => this.onFormSent(), 2000);
    };

    /**
     * Triggers the success animation
     */
    onFormSent = () => {
        this.setState({
            formState: this.opts.formStateSent
        });

        window.setTimeout(() => this.resetForm(), 2000);
    };

    /**
     * Resets the form to empty
     */
    resetForm = () => {
        this.nameInput.current.setState({value: ''});
        this.mailInput.current.setState({value: ''});
        this.messageInput.current.setState({value: ''});

        this.setState({
            formState: null
        });
    };

    render() {
        return (
            <form className={'form' + (this.state.formState ? ' is--' + this.state.formState : '')}
                  onSubmit={this.evaluateForm}
                  ref={this.el}>
                <Input id={'name'} label={'Your name'} required={true} ref={this.nameInput} />
                <Input id={'mail'} label={'Your e-mail address'} type={'email'} required={true} ref={this.mailInput} />
                <Input id={'message'} label={'Your message'} type={'textarea'} required={true} ref={this.messageInput} />
                <Button type={'submit'} text={'Contact Us!'} />
                <div className="form__loader"> </div>
            </form>
        );
    }
}


class Button extends React.Component {
    render() {
        return (
            <div className="button">
                <button type={this.props.type} className="button__element">
                    {this.props.text}
                </button>
            </div>
        );
    }
}



class Input extends React.Component {
    constructor(props) {
        super(props);

        this.opts = {
            'elementTypeInput': 'input',
            'elementTypeTextarea': 'textarea',
        };

        this.state = {
            value: ''
        };
    }

    /**
     * Pushes inputs into the state
     *
     * @param event
     */
    onChange = (event) => {
        this.setState({value: event.target.value});
    };

    render() {
        let InputEl = this.opts.elementTypeInput,
            InputType = this.props.type;

        if (this.props.type === this.opts.elementTypeTextarea) {
            InputEl = this.opts.elementTypeTextarea;
            InputType = null;
        }

        return (
            <div className="input">
                <label className="input__label" htmlFor={this.props.id}>{this.props.label}</label>
                <InputEl
                    type={InputType}
                    id={this.props.id}
                    name={this.props.id}
                    className={"input__field" + (this.props.type === this.opts.elementTypeTextarea ? " is--large" : "")}
                    value={this.state.value}
                    required={this.props.required}
                    onChange={this.onChange.bind(this)}
                />
            </div>
        );
    }
}

ReactDOM.render(<App />, document.getElementById('root'));
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/react@16/umd/react.production.min.js
  2. https://unpkg.com/react-dom@16/umd/react-dom.production.min.js