Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CSS floating action button

Tags:

html

jquery

css

I'm trying to make a button that other options pop out of when you hover over it. I've been searching for a way to do something like the screenshot below, but I wasn't able to do it. It's for a web page, so it needs to be done using HTML, CSS, and jQuery.

Screenshot of button

like image 419
Ahamedos Avatar asked Mar 23 '15 11:03

Ahamedos


People also ask

How do I make a floating action button in CSS?

So we add the CSS rules, set the position fixed, 50px from bottom and 50px from the right. Add the cursor:pointer so it will look like a button. Then we add the <div class=”button iconbutton”> to wrap the fontawesome plus icon. iconbutton is to create the circle icon button with pink color and have shadow under it.

How do you implement a floating action button?

Add the floating action button to your layoutThe size of the FAB, using the app:fabSize attribute or the setSize() method. The ripple color of the FAB, using the app:rippleColor attribute or the setRippleColor() method. The FAB icon, using the android:src attribute or the setImageDrawable() method.

How do I float a button to the right CSS?

If you want to move the button to the right, you can also place the button within a <div> element and add the text-align property with its "right" value to the "align-right" class of the <div>.

What is a floating action button?

Floating action buttons (or FAB) are: “A special case of promoted actions. They are distinguished by a circled icon floating above the UI and have special motion behaviors, related to morphing, launching, and its transferring anchor point.”


2 Answers

The best tutorial for this purpose that I have found so far: https://www.youtube.com/watch?v=iYFNaeEeCDI

Working demo example: https://verkkonet.com/downloads/demo.php?id=c9

.fab-container {
  position: fixed;
  bottom: 50px;
  right: 50px;
  z-index: 999;
  cursor: pointer;
}

.fab-icon-holder {
  width: 50px;
  height: 50px;
  border-radius: 100%;
  background: #016fb9;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
}

.fab-icon-holder:hover {
  opacity: 0.8;
}

.fab-icon-holder i {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  font-size: 25px;
  color: #ffffff;
}

.fab {
  width: 60px;
  height: 60px;
  background: #d23f31;
}

.fab-options {
  list-style-type: none;
  margin: 0;
  position: absolute;
  bottom: 70px;
  right: 0;
  opacity: 0;
  transition: all 0.3s ease;
  transform: scale(0);
  transform-origin: 85% bottom;
}

.fab:hover+.fab-options,
.fab-options:hover {
  opacity: 1;
  transform: scale(1);
}

.fab-options li {
  display: flex;
  justify-content: flex-end;
  padding: 5px;
}

.fab-label {
  padding: 2px 5px;
  align-self: center;
  user-select: none;
  white-space: nowrap;
  border-radius: 3px;
  font-size: 16px;
  background: #666666;
  color: #ffffff;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
  margin-right: 10px;
}
<div class="fab-container">
  <div class="fab fab-icon-holder">
    <i class="fas fa-question"></i>
  </div>
  <ul class="fab-options">
    <li>
      <span class="fab-label">Documentation</span>
      <div class="fab-icon-holder">
        <i class="fas fa-file-alt"></i>
      </div>
    </li>
    <li>
      <span class="fab-label">Video Tutorials</span>
      <div class="fab-icon-holder">
        <i class="fas fa-video"></i>
      </div>
    </li>
    <li>
      <span class="fab-label">Live Chat</span>
      <div class="fab-icon-holder">
        <i class="fas fa-comments"></i>
      </div>
    </li>
    <li>
      <span class="fab-label">Feedback</span>
      <div class="fab-icon-holder">
        <i class="fas fa-comment-alt"></i>
      </div>
    </li>
  </ul>
</div>

The entire example is from: https://verkkonet.com/. The YouTube video tutorial is great to learn how to do this.

like image 110
Bruno Leitão Avatar answered Oct 04 '22 01:10

Bruno Leitão


UPDATE 5/3/2021

I revisited my solution today and added the final touches to those parts I left incomplete yesterday. I also added several additional enhancements and stylistic improvements. Now, for the #mega-button parent, it uses a gradient background but then smoothly transitions to a faded blue-gray background color by using an oversized gradient and transitioning the background-position property. I also decrease the size of this button on hover as I rotate and increase the size of the child .sub-button elements, to give the appearance of the .sub-button elements rolling out from behind the #mega-button.

To make the transition feel more smooth and natural, I also added a slight transition-delay between each of the .sub-button elements to create a waterfall effect rather than them all rolling out on top of each other. I achieved this and also the unique position per .sub-button using a for loop in my SCSS, and iterating in reverse for the transition-delay to have the right-most button animate out first.

I also utilize some glassmorphism styles to give the #mega-button just the slightest bit of a a frosted glass look as the buttons roll out from behind it: backdrop-filter: blur(4px)

When you click on any of the child buttons, they trigger their associated modal via :target. The modals themselves all offer several pure-HTML/CSS ways to close the modal:

  • clicking the translucent background behind the modal
  • clicking the close button "x" in the top-right
  • clicking the "Cancel" action button

Each modal is entirely unique and is triggered by its associated .sub-button. I built a bit of a system behind the modal styles as well, with global styles for the .modal-content, .modal-header, and .modal-body, as well as contextual styles for the .modal-close class, which displays as the clickable/cancellable backdrop behind the modal when inserted within the modal's main element, whereas it displays as the "x" in the top-right corner when included within the .modal-content element. Clicking either will close the modal.

Two final nice-to-haves I added today:

  • Styles for a "new post" form with dynamic styles to intelligently produce the desired amount of spacing, as well as generic styles for a short alert modal, used for the Archive and Delete modals
  • A hover tooltip which displays over the #mega-button. This tooltip cannot be clicked and does not functionally obstruct any content behind it as it has its pointer-events property set to none. This fades to opacity: 0 both when the #mega-button or any of its descendants are being hovered over or when a modal is open, via the general sibling combinator (~).

Here is the finished product:

@charset "UTF-8";
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap");
.modal .modal-content .modal-body {
  -ms-overflow-style: none;
  scrollbar-width: none;
}
.modal .modal-content .modal-body::-webkit-scrollbar {
  display: none;
}
body {
  margin: 0;
  background-color: #eceff1;
  font-family: "Poppins", sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
.warning {
  display: block !important;
  padding: 12px 15px !important;
  background-color: rgba(244, 67, 54, 0.25) !important;
  border-radius: 8px !important;
  font-weight: 500 !important;
  color: #e53935 !important;
  text-align: left !important;
}
.warning > i {
  position: relative;
  top: 1px;
  margin-right: 8px;
  transform: scale(1.25);
}
#mega-button {
  display: flex;
  align-items: center;
  justify-content: center;
  position: fixed;
  bottom: 15px;
  left: 15px;
  width: 50px;
  height: 50px;
  cursor: default;
  transition: all 0.15s ease-out;
  z-index: 1;
  will-change: width;
}
#mega-button > .tooltip {
  padding: 5px 10px;
  position: absolute;
  left: 10px;
  top: -9px;
  transform: translateY(-100%);
  white-space: nowrap;
  background-color: #fff;
  border-radius: 8px;
  filter: drop-shadow(0 2px 2px rgba(120, 144, 156, 0.5));
  box-shadow: inset 0 0 0 1px rgba(120, 144, 156, 0.1);
  font-weight: 500;
  color: #1e4989;
  -webkit-animation: tooltip-hover;
          animation: tooltip-hover;
  /* @keyframes duration | easing-function | delay |
  iteration-count | direction | fill-mode | play-state | name */
  -webkit-animation: 1s ease-in-out 0s infinite alternate both tooltip-hover;
          animation: 1s ease-in-out 0s infinite alternate both tooltip-hover;
  transition: all 0.15s ease-out;
  pointer-events: none;
  -webkit-user-select: none;
     -moz-user-select: none;
      -ms-user-select: none;
          user-select: none;
  will-change: opacity;
}
#mega-button > .tooltip::before {
  content: "";
  display: block;
  position: absolute;
  bottom: 1px;
  left: 8px;
  width: 12px;
  height: 6px;
  background-color: inherit;
  -webkit-clip-path: polygon(0 0, 100% 0, 50% 100%);
          clip-path: polygon(0 0, 100% 0, 50% 100%);
  transform: translateY(100%);
}
#mega-button::before {
  display: flex;
  align-items: center;
  justify-content: center;
  position: absolute;
  left: 0;
  top: 0;
  width: 50px;
  height: 50px;
  padding-top: 4px;
  background: 100% 100%/250% 100% #fff linear-gradient(135deg, transparent 33%, #2c92c8 66%, #892cc8) no-repeat;
  border-radius: 50%;
  content: "";
  font-family: "Font Awesome 5 Pro";
  font-size: 32px;
  font-weight: 400;
  color: #fff;
  transition: inherit;
  box-sizing: border-box;
  cursor: inherit;
  box-shadow: 0 10px 20px -10px #1a237e;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  will-change: transform, background-color, box-shadow;
}
#mega-button > .sub-button {
  display: flex;
  align-items: center;
  justify-content: center;
  position: absolute;
  top: 50%;
  left: 25px;
  width: 36px;
  height: 36px;
  background: #2c92c8;
  border-radius: 50%;
  text-decoration: none !important;
  box-shadow: 0 10px 20px -10px #1a237e;
  transform: translate(-50%, -50%) scale(0.75);
  transition: inherit;
  z-index: -1;
  will-change: transform, transition-duration;
}
#mega-button > .sub-button::before {
  font-family: "Font Awesome 5 Pro";
  color: #fff;
  font-size: 20px;
  font-weight: 400;
  transform: rotate(-90deg);
  transition: inherit;
  will-change: transform;
}
#mega-button > .sub-button#buttons--write::before {
  content: "";
}
#mega-button > .sub-button#buttons--archive::before {
  content: "";
}
#mega-button > .sub-button#buttons--delete::before {
  content: "";
}
#mega-button:hover {
  width: calc(50px + 2px + 123px);
}
#mega-button:hover::before {
  transform: rotate(45deg) scale(0.675);
  padding-right: 2px;
  box-shadow: 7.5px 7.5px 20px -10px rgba(55, 71, 79, 0);
  background: -100% 100%/250% 100% rgba(144, 164, 174, 0.625) linear-gradient(135deg, transparent 33%, #2c92c8 66%, #892cc8) no-repeat;
}
#mega-button:hover::after {
  width: 200px;
}
#mega-button:hover > .sub-button::before {
  transform: rotate(0deg);
}
#mega-button:hover > .sub-button:nth-of-type(1) {
  transform: translate(calc(-50% + 50px + 0% + 0px + 2px), -50%) scale(1);
  transition-delay: 0.1s;
}
#mega-button:hover > .sub-button:nth-of-type(1):hover {
  transform: translate(calc(-50% + 50px + 0% + 0px + 2px), -50%) scale(1.18);
  transition-duration: 0.15s;
}
#mega-button:hover > .sub-button:nth-of-type(2) {
  transform: translate(calc(-50% + 50px + 100% + 5px + 2px), -50%) scale(1);
  transition-delay: 0.05s;
}
#mega-button:hover > .sub-button:nth-of-type(2):hover {
  transform: translate(calc(-50% + 50px + 100% + 5px + 2px), -50%) scale(1.18);
  transition-duration: 0.15s;
}
#mega-button:hover > .sub-button:nth-of-type(3) {
  transform: translate(calc(-50% + 50px + 200% + 10px + 2px), -50%) scale(1);
  transition-delay: 0s;
}
#mega-button:hover > .sub-button:nth-of-type(3):hover {
  transform: translate(calc(-50% + 50px + 200% + 10px + 2px), -50%) scale(1.18);
  transition-duration: 0.15s;
}
#mega-button:hover > .sub-button:hover {
  background-color: #3949ab;
}
#mega-button:hover > .sub-button:hover::before {
  transform: scale(0.85);
}
#mega-button:hover .tooltip, .modal:target ~ #mega-button .tooltip {
  opacity: 0;
}
.modal {
  display: none;
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: transparent;
  z-index: 2;
}
.modal > .modal-close::before {
  content: "";
  display: block;
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  background-color: rgba(38, 50, 56, 0.5);
}
.modal .modal-content {
  display: flex;
  flex-direction: column;
  position: fixed;
  left: 50%;
  top: 50%;
  width: 90%;
  max-width: 600px;
  max-height: 90%;
  overflow: visible;
  transform: translate(-50%, -50%);
}
.modal .modal-content > div:first-of-type {
  border-top-left-radius: 15px;
  border-top-right-radius: 15px;
}
.modal .modal-content > div:last-of-type {
  border-bottom-left-radius: 15px;
  border-bottom-right-radius: 15px;
}
.modal .modal-content > .modal-close::after {
  content: "";
  display: flex;
  align-items: center;
  justify-content: center;
  position: absolute;
  right: 0;
  top: 0;
  width: 28px;
  height: 28px;
  z-index: -1;
  background-color: #546e7a;
  border-radius: 50%;
  font-family: "Font Awesome 5 Pro";
  font-size: 18px;
  font-weight: 400;
  color: #fff;
  transform: translate(37.5%, -37.5%);
  transition: all 0.15s ease-out;
  z-index: 1;
}
.modal .modal-content > .modal-close ~ div:first-of-type {
  border-top-right-radius: 0;
}
.modal .modal-content > .modal-close:hover::after {
  background-color: #263238;
}
.modal .modal-content .modal-header {
  padding: 10px 5px;
  background-color: #37474f;
  font-size: 16px;
  font-weight: 600;
  color: #fff;
  text-transform: uppercase;
  letter-spacing: 2px;
  text-align: center;
}
.modal .modal-content .modal-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 15px;
  overflow-y: auto;
  background-color: #eceff1;
}
.modal .modal-content .modal-body p {
  color: #455a64;
  text-align: center;
}
.modal .modal-content .modal-body p + p, .modal .modal-content .modal-body p + .modal-actions {
  margin-top: 0;
}
.modal .modal-content .modal-body > :first-child {
  margin-top: 0;
}
.modal .modal-content .modal-body label {
  margin-bottom: 4px;
  font-weight: 500;
  color: #78909c;
}
.modal .modal-content .modal-body label:not(:first-of-type) {
  margin-top: 10px;
}
.modal .modal-content .modal-body input, .modal .modal-content .modal-body textarea {
  font-family: inherit;
}
.modal .modal-content .modal-body input:focus, .modal .modal-content .modal-body textarea:focus {
  outline: 0 !important;
}
.modal .modal-content .modal-body input {
  display: block;
  width: 235px;
  padding: 4px 10px;
  align-self: flex-start;
  -webkit-appearance: none;
     -moz-appearance: none;
          appearance: none;
  background-color: #303f9f;
  border: none;
  border-radius: 10px;
  font-size: 16px;
  font-weight: 400;
  line-height: 26px;
  color: #455a64;
  box-sizing: border-box;
  cursor: pointer;
  transition: all 0.15s ease-out;
  will-change: width, padding, background-color, border-radius, font-size, font-weight;
}
.modal .modal-content .modal-body input::-webkit-input-placeholder {
  font-family: "Font Awesome 5 Pro", "Poppins", sans-serif;
  font-size: inherit;
  font-weight: 500;
  color: #fff;
  -webkit-transition: inherit;
  transition: inherit;
  will-change: font-size, font-weight, color;
}
.modal .modal-content .modal-body input:-moz-placeholder {
  font-family: "Font Awesome 5 Pro", "Poppins", sans-serif;
  font-size: inherit;
  font-weight: 500;
  color: #fff;
  -moz-transition: inherit;
  transition: inherit;
  will-change: font-size, font-weight, color;
}
.modal .modal-content .modal-body input::-moz-placeholder {
  font-family: "Font Awesome 5 Pro", "Poppins", sans-serif;
  font-size: inherit;
  font-weight: 500;
  color: #fff;
  -moz-transition: inherit;
  transition: inherit;
  will-change: font-size, font-weight, color;
}
.modal .modal-content .modal-body input:-ms-input-placeholder {
  font-family: "Font Awesome 5 Pro", "Poppins", sans-serif;
  font-size: inherit;
  font-weight: 500;
  color: #fff;
  -ms-transition: inherit;
  transition: inherit;
  will-change: font-size, font-weight, color;
}
.modal .modal-content .modal-body input:focus, .modal .modal-content .modal-body input:valid {
  width: 100%;
  padding: 4px 0;
  background-color: transparent;
  border-radius: 0;
  cursor: text;
}
.modal .modal-content .modal-body input:focus::-webkit-input-placeholder, .modal .modal-content .modal-body input:valid::-webkit-input-placeholder {
  font-weight: inherit;
  color: #78909c;
}
.modal .modal-content .modal-body input:focus:-moz-placeholder, .modal .modal-content .modal-body input:valid:-moz-placeholder {
  font-weight: inherit;
  color: #78909c;
}
.modal .modal-content .modal-body input:focus::-moz-placeholder, .modal .modal-content .modal-body input:valid::-moz-placeholder {
  font-weight: inherit;
  color: #78909c;
}
.modal .modal-content .modal-body input:focus:-ms-input-placeholder, .modal .modal-content .modal-body input:valid:-ms-input-placeholder {
  font-weight: inherit;
  color: #78909c;
}
.modal .modal-content .modal-body input:valid:not(:focus) {
  font-size: 20px;
  font-weight: 500;
}
.modal .modal-content .modal-body textarea {
  display: block;
  min-height: 150px;
  padding: 8px 10px;
  -webkit-appearance: none;
     -moz-appearance: none;
          appearance: none;
  resize: vertical;
  background-color: #fff;
  border: none;
  border-radius: 10px;
  box-shadow: 0 5px 10px -5px #37474f;
  font-size: 16px;
  font-weight: 400;
  line-height: 18px;
  color: #455a64;
  box-sizing: border-box;
}
.modal .modal-content .modal-body textarea::-webkit-input-placeholder {
  font-family: "Font Awesome 5 Pro", "Poppins", sans-serif;
  font-weight: inherit;
  color: #78909c;
}
.modal .modal-content .modal-body textarea:-moz-placeholder {
  font-family: "Font Awesome 5 Pro", "Poppins", sans-serif;
  font-weight: inherit;
  color: #78909c;
}
.modal .modal-content .modal-body textarea::-moz-placeholder {
  font-family: "Font Awesome 5 Pro", "Poppins", sans-serif;
  font-weight: inherit;
  color: #78909c;
}
.modal .modal-content .modal-body textarea:-ms-input-placeholder {
  font-family: "Font Awesome 5 Pro", "Poppins", sans-serif;
  font-weight: inherit;
  color: #78909c;
}
.modal .modal-content .modal-body .modal-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 20px;
}
.modal .modal-content .modal-body .modal-actions a, .modal .modal-content .modal-body .modal-actions button {
  -webkit-appearance: none;
     -moz-appearance: none;
          appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 40px;
  padding: 0 15px;
  background-color: #607d8b;
  border: none;
  border-radius: 20px;
  box-shadow: 0 5px 10px -15px #455a64;
  font-family: inherit;
  font-size: 16px;
  font-weight: 500;
  color: #fff;
  text-decoration: none !important;
  transition: box-shadow 0.15s ease-out;
  will-change: box-shadow;
  cursor: pointer;
}
.modal .modal-content .modal-body .modal-actions a.write, .modal .modal-content .modal-body .modal-actions button.write {
  background-color: #2196f3;
}
.modal .modal-content .modal-body .modal-actions a.archive, .modal .modal-content .modal-body .modal-actions button.archive {
  background-color: #ef6c00;
}
.modal .modal-content .modal-body .modal-actions a.delete, .modal .modal-content .modal-body .modal-actions button.delete {
  background-color: #f44336;
}
.modal .modal-content .modal-body .modal-actions a.write, .modal .modal-content .modal-body .modal-actions button.write {
  box-shadow: 0 5px 10px -15px #0d47a1;
}
.modal .modal-content .modal-body .modal-actions a.archive, .modal .modal-content .modal-body .modal-actions button.archive {
  box-shadow: 0 5px 10px -15px #bf360c;
}
.modal .modal-content .modal-body .modal-actions a.delete, .modal .modal-content .modal-body .modal-actions button.delete {
  box-shadow: 0 5px 10px -15px #b71c1c;
}
.modal .modal-content .modal-body .modal-actions a:hover, .modal .modal-content .modal-body .modal-actions button:hover {
  box-shadow: 0 5px 10px -5px #455a64;
}
.modal .modal-content .modal-body .modal-actions a:hover.write, .modal .modal-content .modal-body .modal-actions button:hover.write {
  box-shadow: 0 5px 10px -5px #0d47a1;
}
.modal .modal-content .modal-body .modal-actions a:hover.archive, .modal .modal-content .modal-body .modal-actions button:hover.archive {
  box-shadow: 0 5px 10px -5px #bf360c;
}
.modal .modal-content .modal-body .modal-actions a:hover.delete, .modal .modal-content .modal-body .modal-actions button:hover.delete {
  box-shadow: 0 5px 10px -5px #b71c1c;
}
.modal .modal-content .modal-body .modal-actions a + a, .modal .modal-content .modal-body .modal-actions a + button, .modal .modal-content .modal-body .modal-actions button + a, .modal .modal-content .modal-body .modal-actions button + button {
  margin-left: 4px;
}
.modal:target {
  display: block;
}
@-webkit-keyframes tooltip-hover {
  from {
    transform: translateY(-100%);
  }
  to {
    transform: translateY(calc(-100% - 5px));
  }
}
@keyframes tooltip-hover {
  from {
    transform: translateY(-100%);
  }
  to {
    transform: translateY(calc(-100% - 5px));
  }
}
<script src="https://kit.fontawesome.com/07afc061fe.js" crossorigin="anonymous"></script>

<div id="modal-write" class="modal">
  <a href="#" class="modal-close"></a>
  <div class="modal-content">
    <a href="#" class="modal-close"></a>
    <div class="modal-header">New Post</div>
    <div class="modal-body">
      <label for="modal-write-title">Post Title</label>
      <input type="text" name="title" id="modal-write-title" placeholder=" Give your post a name..." required>
      <label for="modal-write-body">Post Body</label>
      <textarea name="body" cols="30" rows="10" id="modal-write-body" placeholder="What would you like to say?" required></textarea>
      <div class="modal-actions">
        <a href="#">Cancel</a>
        <button class="write">Post</button>
      </div>
    </div>
  </div>
</div>
<div id="modal-archive" class="modal">
  <a href="#" class="modal-close"></a>
  <div class="modal-content">
    <a href="#" class="modal-close"></a>
    <div class="modal-header">Archive Post</div>
    <div class="modal-body">
      <p>Would you like to archive your post?</p>
      <div class="modal-actions">
        <a href="#">Cancel</a>
        <button class="archive">Archive</button>
      </div>
    </div>
  </div>
</div>
<div id="modal-delete" class="modal">
  <a href="#" class="modal-close"></a>
  <div class="modal-content">
    <a href="#" class="modal-close"></a>
    <div class="modal-header">Delete Post</div>
    <div class="modal-body">
      <p>Are you sure you want to delete your post?</p>
      <p class="warning"><i class="far fa-exclamation-circle"></i>This cannot be undone.</p>
      <div class="modal-actions">
        <a href="#">Cancel</a>
        <button class="delete">Delete</button>
      </div>
    </div>
  </div>
</div>
<div id="mega-button">
  <div class="tooltip">Hover me!</div>
  <a class="sub-button" id="buttons--write" href="#modal-write"></a>
  <a class="sub-button" id="buttons--archive" href="#modal-archive"></a>
  <a class="sub-button" id="buttons--delete" href="#modal-delete"></a>
</div>

Here is the finished CodePen for this solution in all its original "Sass"-y glory: https://codepen.io/brandonmcconnell/pen/082f4ee58d234501757b8d6a748c514b?editors=0100

All of the dynamic variables in this SCSS could rather simply be converted to CSS custom properties if you prefer to keep all those calculations dynamic in the final CSS.

like image 30
Brandon McConnell Avatar answered Oct 04 '22 02:10

Brandon McConnell