Skip to content

EpButton

EpButton is a button. It's very exciting. It's used in many places throughout Epicenter Design System. Okay, just a couple places. But it could be used in many places.

Usage

Basic Button

vue
<template>
  <ep-button @click="handleClick">
    Click me
  </ep-button>
</template>

<script setup>
  import { EpButton } from '@epicenter/vue-components'

  const handleClick = () => {
    console.log('Button clicked!')
  }
</script>

With Icons

vue
<template>
  <ep-button>
    <template #icon-left>
      <icon-plus />
    </template>
    Add Item
    <template #icon-right>
      <icon-arrow-right />
    </template>
  </ep-button>
</template>

<script setup>
  import { EpButton } from '@epicenter/vue-components'
  import { IconPlus, IconArrowRight } from '@epicenter/icons'
</script>

Sizes and States

vue
<template>
  <ep-button size="small">Small</ep-button>
  <ep-button size="default">Default</ep-button>
  <ep-button size="large">Large</ep-button>
  <ep-button size="xlarge">X-Large</ep-button>
  <ep-button disabled>Disabled</ep-button>
</template>

<script setup>
  import { EpButton } from '@epicenter/vue-components'
</script>

Props

NameDescriptionTypeDefault
sizeThe size of the button.string'default'
ariaLabelThe aria-label of the button.string''
disabledIf true, the button will be disabled.booleanfalse
typeThe type of the button.string'button'

Slots

NameDescription
icon-leftIcon displayed on the left side of the button label
defaultDefault slot for button text/content
icon-rightIcon displayed on the right side of the button label

INFO

This component does not use events.

Component Code

vue
<template>
  <component
    :is="element"
    :type
    :class="['ep-button', computedClasses]"
    :aria-label="ariaLabel ? ariaLabel : null"
    :disabled="disabled"
  >
    <span
      v-if="$slots['icon-left']"
      class="ep-button__icon ep-button__icon--left"
    >
      <!-- @slot Icon displayed on the left side of the button label -->
      <slot name="icon-left" />
    </span>
    <span
      v-if="$slots.default"
      class="ep-button__label"
    >
      <!-- @slot Default slot for button text/content -->
      <slot />
    </span>
    <span
      v-if="$slots['icon-right']"
      class="ep-button__icon ep-button__icon--right"
    >
      <!-- @slot Icon displayed on the right side of the button label -->
      <slot name="icon-right" />
    </span>
  </component>
</template>

<script setup>
  import { computed, useAttrs } from 'vue'

  defineOptions({
    name: 'EpButton'
  })

  const props = defineProps({
    /**
     * The size of the button.
     */
    size: {
      type: String,
      default: 'default',
      validator: (value) => [
        'small',
        'default',
        'large',
        'xlarge',
      ].includes(value)
    },
    /**
     * The aria-label of the button.
     */
    ariaLabel: {
      type: String,
      default: ''
    },
    /**
     * If `true`, the button will be disabled.
     */
    disabled: {
      type: Boolean,
      default: false
    },
    /**
     * The  type of the button.
     */
    type: {
      type: String,
      default: 'button',
      validator: (value) => [
        'button',
        'submit',
      ].includes(value)
    },
  })

  const element = computed(() => {
    const { to, href } = useAttrs()
    return to ? 'router-link' : href ? 'a' : 'button'
  })

  const computedClasses = computed(() => ({
    [`ep-button--${props.size}`]: props.size !== 'default',
    'ep-button--disabled': props.disabled,
  }))
</script>

Styles (SCSS)

scss
@use '../mixins/_mixins' as *;

.ep-button {
  --ep-button-bg-color: var(--interface-foreground);
  --ep-button-text-color: var(--text-color--loud);
  --ep-button-border-width: 0.1rem;
  --ep-button-border-style: solid;
  --ep-button-border-color: var(--border-color);
  --ep-button-border-radius: 0.3rem;
  --ep-button-hover-bg-color: var(--interface-foreground);
  --ep-button-hover-text-color: var(--text-color--loud);
  --ep-button-hover-border-color: var(--border-color);
  --ep-button-active-bg-color: var(--interface-foreground);
  --ep-button-active-text-color: var(--text-color);
  --ep-button-active-border-color: var(--border-color);
  --ep-button-selected-bg-color: var(--primary-color-up-15-base);
  --ep-button-selected-border-color: var(--primary-color-base);
  --ep-button-selected-text-color: var(--text-color--loud);
  --ep-button-disabled-bg-color: var(--interface-foreground);
  --ep-button-disabled-text-color: var(--text-color--disabled);
  --ep-button-disabled-border-color: var(--border-color--disabled);
  --ep-button-padding-inline: 1.2rem;
  display: inline-flex;
  justify-content: space-between;
  align-items: center;
  gap: calc(var(--ep-button-padding-inline) / 1.5);
  max-width: max-content;
  height: 3rem;
  border-radius: var(--ep-button-border-radius);
  font-size: var(--font-size--small);
  line-height: 1;
  appearance: none;
  text-decoration: none;
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  vertical-align: top;
  background: var(--ep-button-bg-color);
  color: var(--ep-button-text-color);
  padding-inline: var(--ep-button-padding-inline);
  border-width: var(--ep-button-border-width);
  border-style: var(--ep-button-border-style);
  border-color: var(--ep-button-border-color);

  @include hover {
    &:not([class$='--selected']):not(.ep-button--disabled):hover {
      background: var(--ep-button-hover-bg-color);
      color: var(--ep-button-hover-text-color);
      border-color: var(--ep-button-hover-border-color);
    }
  }

  &:not([class$='--selected']):active {
    background: var(--ep-button-active-bg-color);
    color: var(--ep-button-active-text-color);
    border-color: var(--ep-button-active-border-color);
  }

  &--disabled {
    background: var(--ep-button-disabled-bg-color);
    color: var(--ep-button-disabled-text-color);
    border-color: var(--ep-button-disabled-border-color);
    pointer-events: none;
    cursor: default;

    &.ep-button--menu-item {
      border-color: transparent;
    }
  }

  &--selected {
    background: var(--ep-button-selected-bg-color);
    border-color: var(--ep-button-selected-border-color);
    color: var(--ep-button-selected-text-color);
  }

  &__icon {
    display: inline-flex;
    justify-content: center;
    align-items: center;
    height: 70%;
    max-height: 2.4rem;
    pointer-events: none;
  }

  &__label {
    flex: 1;
    text-align: left;
    pointer-events: none;
    line-height: 2rem;
  }

  // default 30px height
  // has only one child
  &:has(:only-child) {

    // and it's an icon
    &:has(.ep-button__icon) {
      --ep-button-padding-inline: 0.8rem;
    }

    // and it's a label
    &:has(.ep-button__label) {
      --ep-button-padding-inline: 1.2rem;
    }
  }

  // has two children, one icon left and one label
  &:has(.ep-button__icon--left, .ep-button__label) {
    --ep-button-padding-inline: 0.8rem 1.2rem;
  }

  // has two children, one icon right and one label
  &:has(.ep-button__icon--right, .ep-button__label) {
    --ep-button-padding-inline: 1.2rem 0.8rem;
  }

  // has icon left, icon right, and label
  &:has(.ep-button__icon--left, .ep-button__icon--right, .ep-button__label) {
    --ep-button-padding-inline: 0.8rem;
  }
}

// small 22px height
.ep-button--small {
  gap: 0.4rem;
  height: 2.2rem;

  .ep-button__label {
    font-size: var(--font-size--tiny);
  }

  // padding
  // has only one child
  &:has(:only-child) {

    // and it's an icon
    &:has(.ep-button__icon) {
      --ep-button-padding-inline: 0.3rem;
    }

    // and it's a label
    &:has(.ep-button__label) {
      --ep-button-padding-inline: 0.6rem;
    }
  }

  // has two children, one icon left and one label
  &:has(.ep-button__icon--left, .ep-button__label) {
    --ep-button-padding-inline: 0.3rem 0.6rem;
  }

  // has two children, one icon right and one label
  &:has(.ep-button__icon--right, .ep-button__label) {
    --ep-button-padding-inline: 0.6rem 0.3rem;
  }

  // has icon left, icon right, and label
  &:has(.ep-button__icon--left, .ep-button__icon--right, .ep-button__label) {
    --ep-button-padding-inline: 0.6rem;
  }
}

// large 38px height
.ep-button--large {
  // max-height: 3.8rem;
  height: 3.8rem;

  .ep-button__label {
    font-size: var(--font-size--default);
  }

  // padding
  // has only one child
  &:has(:only-child) {

    // and it's an icon
    &:has(.ep-button__icon) {
      --ep-button-padding-inline: 1rem;
    }

    // and it's a label
    &:has(.ep-button__label) {
      --ep-button-padding-inline: 1.5rem;
    }
  }

  // has two children, one icon left and one label
  &:has(.ep-button__icon--left, .ep-button__label) {
    --ep-button-padding-inline: 0.8rem 1.5rem;
  }

  // has two children, one icon right and one label
  &:has(.ep-button__icon--right, .ep-button__label) {
    --ep-button-padding-inline: 1.5rem 0.8rem;
  }

  // has icon left, icon right, and label
  &:has(.ep-button__icon--left, .ep-button__icon--right, .ep-button__label) {
    --ep-button-padding-inline: 0.8rem;
  }
}

// xlarge 46px height
.ep-button--xlarge {
  // max-height: 4.6rem;
  height: 4.6rem;

  .ep-button__label {
    font-size: var(--font-size--default);
  }

  // padding
  // has only one child
  &:has(:only-child) {

    // and it's an icon
    &:has(.ep-button__icon) {
      --ep-button-padding-inline: 1.2rem;
    }

    // and it's a label
    &:has(.ep-button__label) {
      --ep-button-padding-inline: 1.8rem;
    }
  }

  // has two children, one icon left and one label
  &:has(.ep-button__icon--left, .ep-button__label) {
    --ep-button-padding-inline: 1.2rem 1.8rem;
  }

  // has two children, one icon right and one label
  &:has(.ep-button__icon--right, .ep-button__label) {
    --ep-button-padding-inline: 1.8rem 1.2rem;
  }

  // has icon left, icon right, and label
  &:has(.ep-button__icon--left, .ep-button__icon--right, .ep-button__label) {
    --ep-button-padding-inline: 1.8rem;
  }
}