Skip to content

Snackbar Component Code

Dependencies

This component requires:

  • Vue 3 with Composition API
  • useSnackbar composable
  • uuid package for unique ID generation
  • convertRemToPixels inject (optional)

Full Component Code

vue
<script setup>
import { computed, inject } from 'vue';
import { useSnackbar } from '@/composables/useSnackbarComposable.js';

const convertRemToPixels = inject('convertRemToPixels', null);

const { notification, config, hasNotification, removeNotification } = useSnackbar();

// Handle close notification
const closeNotification = () => {
  // You can add any additional logic here before removing
  // For example: analytics, logging, etc.
  removeNotification();
};

// Use individual notification position or fall back to global config
const notificationPosition = computed(() => {
  return notification.value?.position || config.value.position;
});

const containerStyle = computed(() => {
  let { x, y } = config.value.offset;

  // Convert rem to pixels
  x = convertRemToPixels ? convertRemToPixels(x) : x * parseInt(getComputedStyle(document.documentElement).fontSize);
  y = convertRemToPixels ? convertRemToPixels(y) : y * parseInt(getComputedStyle(document.documentElement).fontSize);

  // Use individual notification position or fall back to global config
  const position = notification.value?.position || config.value.position;
  const styles = {};

  // Apply positioning based on notification position
  switch (position) {
    case 'top-right':
      styles.top = `${y}px`;
      styles.right = `${x}px`;
      break;
    case 'top-left':
      styles.top = `${y}px`;
      styles.left = `${x}px`;
      break;
    case 'top':
      styles.top = `${y}px`;
      styles.left = '50%';
      styles.transform = 'translateX(-50%)';
      break;
    case 'bottom-right':
      styles.bottom = `${y}px`;
      styles.right = `${x}px`;
      break;
    case 'bottom-left':
      styles.bottom = `${y}px`;
      styles.left = `${x}px`;
      break;
    case 'bottom':
      styles.bottom = `${y}px`;
      styles.left = '50%';
      styles.transform = 'translateX(-50%)';
      break;
  }

  return styles;
});
</script>

<template>
  <div v-if="notification" :class="['snackbar-container', `position-${notificationPosition}`]" :style="containerStyle">
    <div :class="['snackbar-notification', ...notification.notificationClass]">
      <div class="snackbar-content">
        <div class="snackbar-icon" v-if="notification.iconRef" v-html="notification.iconRef"></div>
        <div class="snackbar-messages">
          <div class="snackbar-primary-message">{{ notification.primaryMessage }}</div>
          <div v-if="notification.secondaryMessage" class="snackbar-secondary-message">
            {{ notification.secondaryMessage }}
          </div>
        </div>
        <button
          v-if="notification.showCloseButton"
          @click="closeNotification"
          class="snackbar-close-button"
          aria-label="Close notification"
        >
          <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path
              d="M10.5 3.5L3.5 10.5M3.5 3.5L10.5 10.5"
              stroke="currentColor"
              stroke-width="1.5"
              stroke-linecap="round"
              stroke-linejoin="round"
            />
          </svg>
        </button>
      </div>
    </div>
  </div>
</template>

<style scoped lang="scss">
.snackbar-container {
  position: fixed;
  z-index: 9999;
  background: transparent;
}

.snackbar-notification {
  min-width: 18.75rem;
  max-width: 25rem;
  background: white;
  border-radius: 8px;
  border: 1px solid #e5e7eb;
  overflow: hidden;

  .snackbar-content {
    display: flex;
    align-items: center;
    padding: 0.75rem 1rem;
    gap: 0.75rem;
  }

  .snackbar-icon {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-top: 0.125rem;
  }

  .snackbar-messages {
    flex: 1;
    min-width: 0;
  }

  .snackbar-primary-message {
    font-size: 0.875rem;
    font-weight: 500;
    color: #111827;
    margin: 0;
    line-height: 1.4;
  }

  .snackbar-secondary-message {
    font-size: 0.8125rem;
    color: #6b7280;
    margin: 0.25rem 0 0 0;
    line-height: 1.4;
  }

  .snackbar-close-button {
    flex-shrink: 0;
    background: none;
    border: none;
    cursor: pointer;
    padding: 0.25rem;
    border-radius: 0.25rem;
    color: #9ca3af;
    transition: all 0.2s ease;

    &:hover {
      color: #6b7280;
      background-color: #f3f4f6;
    }
  }

  // Type-specific styling
  &.success {
    border-left: 4px solid #10b981;
    .snackbar-content {
      .snackbar-primary-message {
        color: #10b981;
      }
    }
  }

  &.error {
    border-left: 4px solid #ef4444;
    .snackbar-content {
      .snackbar-primary-message {
        color: #ef4444;
      }
    }
  }

  &.warning {
    border-left: 4px solid #f59e0b;
    .snackbar-content {
      .snackbar-primary-message {
        color: #f59e0b;
      }
    }
  }

  &.info {
    border-left: 4px solid #3b82f6;
    .snackbar-content {
      .snackbar-primary-message {
        color: #3b82f6;
      }
    }
  }
}
</style>