React and Vue examples

Update button and icon classes based on download state, and share naming configuration across components.

These examples work in React with TypeScript or Vue 3.3+. Install @lai9fox/bem, choose the component for your framework, and add button.css below.

React: download button

Save as DownloadButton.tsx. The parent handles downloads through onDownload and passes loading={true} while downloading. The class name generator is created outside the component and uses props to generate classes on each render.

import { createBem } from "@lai9fox/bem";
import "./button.css";

const button = createBem({ namespace: "ui-" }).block("button");

type DownloadButtonProps = {
  disabled?: boolean;
  loading?: boolean;
  onDownload: () => void;
};

export function DownloadButton({
  disabled = false,
  loading = false,
  onDownload,
}: DownloadButtonProps) {
  return (
    <button
      type="button"
      disabled={disabled || loading}
      aria-busy={loading}
      onClick={onDownload}
      className={button.classes({
        block: true,
        modifiers: { disabled: disabled || loading, loading },
      })}
    >
      <span
        aria-hidden="true"
        className={button.classes({
          elements: { icon: true },
          elementModifiers: { icon: { loading } },
        })}
      >
        {loading ? "…" : "↓"}
      </span>
      {loading ? "Downloading" : "Download"}
    </button>
  );
}

Vue: reactive download state

Save as DownloadButton.vue. The parent passes state through :loading="loading" and handles downloads with @download="startDownload". The computed values update class names when the relevant props change.

<script setup lang="ts">
import { computed } from "vue";
import { createBem } from "@lai9fox/bem";
import "./button.css";

const props = withDefaults(
  defineProps<{
    disabled?: boolean;
    loading?: boolean;
  }>(),
  { disabled: false, loading: false },
);

const emit = defineEmits<{ download: [] }>();
const button = createBem({ namespace: "ui-" }).block("button");
const buttonClass = computed(() =>
  button.classes({
    block: true,
    modifiers: {
      disabled: props.disabled || props.loading,
      loading: props.loading,
    },
  }),
);
const iconClass = computed(() =>
  button.classes({
    elements: { icon: true },
    elementModifiers: { icon: { loading: props.loading } },
  }),
);
</script>

<template>
  <button
    type="button"
    :disabled="props.disabled || props.loading"
    :aria-busy="props.loading"
    :class="buttonClass"
    @click="emit('download')"
  >
    <span :class="iconClass" aria-hidden="true">{{
      props.loading ? "…" : "↓"
    }}</span>
    {{ props.loading ? "Downloading" : "Download" }}
  </button>
</template>

Styles and output

Create button.css in the component’s directory:

.ui-button {
  display: inline-flex;
  align-items: center;
  padding: 0.5rem 0.75rem;
  border: 1px solid currentColor;
  background: white;
  color: #14532d;
}

.ui-button--disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.ui-button--loading {
  cursor: wait;
}

.ui-button__icon {
  margin-inline-end: 0.5rem;
}

.ui-button__icon--loading {
  font-weight: 700;
}

During a download, the class names are:

  • Button: ui-button ui-button--disabled ui-button--loading.
  • Icon span: ui-button__icon ui-button__icon--loading.

The disabled attribute disables the button, aria-busy marks it as busy, and classes control styles. The parent manages download state and requests. See the classes() API for condition rules.

Shared configuration

Create a factory in bem.ts. Each component can import it and call bem.block(name). This navigation example adds a modifier class to the current item:

// bem.ts
import { createBem } from "@lai9fox/bem";

export const bem = createBem({ namespace: "ui-" });
// navigation.ts
import { bem } from "./bem";

const nav = bem.block("nav");

export function navigationItemClass(current: boolean) {
  return nav.classes({
    elements: { item: true },
    elementModifiers: { item: { current } },
  });
}

navigationItemClass(true);
// 'ui-nav__item ui-nav__item--current'

The button component can also import this bem and call bem.block("button"). If you change the class name prefix or separators, update the CSS selectors too. See createBem for configuration options.