Modal

@use 'sprucecss/scss/spruce' as *;

.modal-backdrop {
  align-items: start;
  background-color: color('background', 'modal');
  display: flex;
  inset: 0;
  justify-content: center;
  overflow-y: auto;
  position: fixed;
  z-index: 25;
}

.modal {
  $this: &;

  @include set-css-variable((
    --inline-size: 34rem
  ));
  background-color: color('background');
  border: 1px solid color('border');
  border-radius: config('border-radius-sm', $display);
  box-shadow: 0 0 spacer('xxs') hsl(201.15deg 72.03% 32.71% / 5%);
  inline-size: get-css-variable(--inline-size);
  margin: spacer('m');
  max-inline-size: 100%;
  outline: 0;
  position: relative;

  &__header {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: spacer('s');
    justify-content: space-between;
    padding: spacer('s') spacer-clamp('s', 'm') 0;

    &-caption {
      @include layout-stack(0);
    }
  }

  &__title {
    font-size: font-size('h3');
    font-weight: 600;
    margin-block: 0;
  }

  &__body {
    @include layout-stack('s');
    padding: spacer-clamp('s', 'm');
  }

  &__footer {
    align-items: center;
    border-block-start: 1px solid color('border');
    display: flex;
    gap: spacer('s');
    justify-content: end;
    padding: spacer('s') spacer-clamp('s', 'm');

    &--space-between {
      justify-content: space-between;
    }

    input {
      flex-grow: 1;
      max-inline-size: 25rem;
    }
  }
}
<div
    x-data="{
        open: false,
        close() {
            this.open = false;
            $refs.open.focus();
        }
    }"
>
    <button
        x-ref="open"
        class="btn btn--primary"
        @click="open = true"
    >
        Open modal
    </button>
    <template x-teleport="body">
        <div
            class="modal-backdrop"
            x-show="open"
            x-transition.opacity
            @keydown.escape="close"
        >
            <div
                role="dialog"
                aria-modal="true"
                tabindex="0"
                class="modal"
                @click.away="close"
                x-show="open"
                x-transition
                x-trap.noscroll="open"
            >
                <div class="modal__header">
                    <div class="modal__header-caption">
                        <h2 class="modal__title">Sign In</h2>
                        <p class="modal__subtitle">Hey there, welcome back!</p>
                    </div>
                    <button
                        class="btn btn--icon btn--sm btn--light"
                        aria-label="Close modal"
                        @click="close"
                    >
                        <svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" stroke="currentColor" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
                            <line x1="18" y1="6" x2="6" y2="18"></line>
                            <line x1="6" y1="6" x2="18" y2="18"></line>
                        </svg>
                    </button>
                </div>
                <div class="modal__body">
                    <div class="auth-form">
                        <form method="POST" action="#">
                            <div class="form-group-stack">
                                <div class="auth-form__social">
                                    <button class="btn btn--outline-dark btn--icon btn--block">
                                        <svg class="btn__icon" aria-hidden="true" focusable="false" height="100%" version="1.1" viewBox="0 0 24 24" width="100%" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;overflow:visible;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
                                            <g>
                                                <path d="M12.24,9.818l-0,4.647l6.458,0c-0.283,1.495 -1.135,2.76 -2.411,3.611l3.895,3.022c2.269,-2.094 3.578,-5.171 3.578,-8.825c-0,-0.851 -0.076,-1.669 -0.218,-2.455l-11.302,0Z" style="fill:#4285f4;fill-rule:nonzero;" />
                                                <path d="M5.515,14.284l-0.879,0.672l-3.109,2.422c1.975,3.917 6.022,6.622 10.713,6.622c3.24,-0 5.956,-1.069 7.941,-2.902l-3.894,-3.022c-1.069,0.72 -2.433,1.157 -4.047,1.157c-3.12,-0 -5.771,-2.106 -6.72,-4.942l-0.005,-0.007Z" style="fill:#34a853;fill-rule:nonzero;" />
                                                <path d="M1.527,6.622c-0.818,1.614 -1.287,3.436 -1.287,5.378c0,1.942 0.469,3.764 1.287,5.378c0,0.011 3.993,-3.098 3.993,-3.098c-0.24,-0.72 -0.382,-1.484 -0.382,-2.28c0,-0.797 0.142,-1.56 0.382,-2.28l-3.993,-3.098Z" style="fill:#fbbc05;fill-rule:nonzero;" />
                                                <path d="M12.24,4.778c1.767,0 3.338,0.611 4.593,1.789l3.436,-3.436c-2.084,-1.942 -4.789,-3.131 -8.029,-3.131c-4.691,0 -8.738,2.695 -10.713,6.622l3.993,3.098c0.949,-2.836 3.6,-4.942 6.72,-4.942Z" style="fill:#ea4335;fill-rule:nonzero;" />
                                            </g>
                                        </svg>
                                        Sign in with Google
                                    </button>
                                    <button class="btn btn--outline-dark btn--icon btn--block">
                                        <svg class="btn__icon" aria-hidden="true" focusable="false" height="100%" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;fill:currentColor;overflow:visible;" version="1.1" viewBox="0 0 15 18" width="100%" xml:space="preserve" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
                                            <path d="M14.187,6.137c-0.104,0.081 -1.948,1.12 -1.948,3.429c0,2.672 2.346,3.617 2.416,3.64c-0.011,0.058 -0.372,1.294 -1.236,2.555c-0.771,1.108 -1.576,2.216 -2.8,2.216c-1.224,-0 -1.539,-0.711 -2.952,-0.711c-1.377,-0 -1.867,0.734 -2.987,0.734c-1.119,-0 -1.901,-1.026 -2.799,-2.286c-1.04,-1.48 -1.881,-3.779 -1.881,-5.961c0,-3.499 2.275,-5.355 4.515,-5.355c1.19,-0 2.182,0.781 2.929,0.781c0.711,0 1.82,-0.828 3.173,-0.828c0.514,0 2.357,0.047 3.57,1.786Zm-4.212,-3.268c0.56,-0.664 0.956,-1.585 0.956,-2.507c-0,-0.128 -0.011,-0.258 -0.035,-0.362c-0.91,0.034 -1.994,0.607 -2.648,1.365c-0.513,0.583 -0.991,1.504 -0.991,2.439c-0,0.14 0.023,0.281 0.034,0.326c0.057,0.01 0.151,0.023 0.245,0.023c0.817,0 1.845,-0.547 2.439,-1.284Z" style="fill-rule:nonzero;" />
                                        </svg>
                                        Sign in with Apple ID
                                    </button>
                                </div>
                                <div class="auth-form__separator">or</div>
                                <div class="form-group">
                                    <label class="form-label" for="email">Email</label>
                                    <input class="form-control" id="email" type="text" name="email" required="required" autocomplete="off" autofocus="autofocus"/>
                                </div>
                                <div class="form-group">
                                    <label class="form-label form-label--space-between" for="password">Password <a href="#">Forgot your password?</a>
                                    </label>
                                    <input class="form-control" id="password" type="password" name="password" required="required" autocomplete="off"/>
                                </div>
                                <div class="form-group">
                                    <label class="form-check form-check--lg" for="remember">
                                        <input class="form-check__control" id="remember" type="checkbox" name="remember"/>
                                        <span class="form-label form-check__label">Remember me</span>
                                    </label>
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
                <div class="modal__footer">
                    <button class="btn btn--outline-primary btn--block" @click="close">Cancel</button>
                    <button class="btn btn--primary btn--block btn--primary-shadow">Save</button>
                </div>
            </div>
        </div>
    </template>
</div>

Modal

A generic - animated - modal component wrapper to display content over anything.

Technical Details

  • It is built on Alpine.js because making a proper modal is not that easy without heavy JS.
  • It is animated using x-transition.

Dependencies

  • Alpine.js - Some or all of the components require Alpine.js for functionality.
  • Alpine.js Focus Plugin - You should also consider adding the focus plugin for better focus management.
  • Auth Form - The content preview inside of the modal.

Colors

$colors: (
  'btn': (
    'dark-background': hsl(205deg 100% 2%),
    'dark-background-hover': hsl(205deg 100% 5%),
    'dark-foreground': hsl(0deg 0% 100%),
    'dark-outline-border': hsl(260deg 4% 70%),
    'dark-outline-foreground': hsl(205deg 100% 2%),
    'dark-outline-foreground-hover': hsl(0deg 0% 100%),
    'dark-outline-background-hover': hsl(205deg 100% 2%),
    'dark-outline-focus-ring': hsl(205deg 100% 2%),
  ),
  'modal': (
    'background': hsl(210deg 60% 98% / 90%),
  ),
),

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our extensive UI library built with Spruce CSS.

Blog

Read about front-end development and concepts of Spruce CSS.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.