Skip to content
v2.5.0
English
Русский
Documentation

ButtonBase

ButtonBase is the low-level action primitive behind Button and IconButton.
button-base.tsx · 3 lines
import { ButtonBase } from '@vega-ui/react';

<ButtonBase type='button' variant='secondary' appearance='ghost'>Reset filters</ButtonBase>
Browser support
Newly available 2024
  • Chrome 125
  • Edge 125
  • Firefox 128
  • Safari 18
  • Safari on iOS 18
Bundle size
Tested in
10 tests

Summary

ButtonBase is the low-level action primitive behind Button and IconButton. It owns the shared button styling contract, focus treatment, and asChild composition without adding higher-level product layout rules.

Imports

import {
  ButtonBase,
  type ButtonBaseAppearance,
  type ButtonBaseProps,
  type ButtonBaseVariant,
} from '@vega-ui/react/ButtonBase';

Exported Types

  • ButtonBaseProps
  • ButtonBaseVariant = 'primary' | 'secondary' | string
  • ButtonBaseAppearance = 'fill' | 'outline' | 'ghost' | 'transparent' | string

Basic Usage

basic-usage.tsx · 23 lines
import { ButtonBase } from '@vega-ui/react';
import { useState } from 'react';

export default function BasicUsage() {
  const [likes, setLikes] = useState(24);

  return (
    <ButtonBase
      type='button'
      variant='secondary'
      appearance='ghost'
      onClick={() => setLikes(likes + 1)}

Variants

  • Variant: primary, secondary
  • Appearance: fill, outline, ghost, transparent
  • Rendering mode: native button or asChild

Edge Cases

  • type should be set explicitly for form actions.
  • If asChild is used, the child must accept the forwarded props and keep correct semantics.
  • Icon-only content still needs an accessible name.

Common Mistakes

  • Using ButtonBase when Button or IconButton would be clearer.
  • Forgetting that link semantics remain link semantics under asChild.
  • Treating unsupported variant strings as if CSS already implements them.

Examples

Basic

Basic: variants and appearances

ButtonBase maps variant and appearance to colors and state styles but has no height or padding. Every example here adds its own spacing, which is exactly what Button and IconButton do on top of it.

variant-appearance-matrix.tsx · 22 lines
import { ButtonBase, type ButtonBaseAppearance, type ButtonBaseVariant } from '@vega-ui/react';

const variants: ButtonBaseVariant[] = ['primary', 'secondary'];
const appearances: ButtonBaseAppearance[] = ['fill', 'outline', 'ghost', 'transparent'];

export default function VariantAppearanceMatrix() {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 'var(--spacing-4)', width: '100%' }}>
      {appearances.map((appearance) => variants.map((variant) => (
        <ButtonBase
          key={`${variant}-${appearance}`}
          type='button'

Controlled/Stateful

Controlled/Stateful: filter chips with aria-pressed

Pill-shaped chips are a typical custom surface built on ButtonBase. Each chip is a toggle with aria-pressed, the parent owns the selected filters, and the list below reacts to them.

filter-chips.tsx · 64 lines
import { ButtonBase } from '@vega-ui/react';
import { useState } from 'react';

const tasks = [
  { title: 'Draft Q3 roadmap', status: 'open' },
  { title: 'Review onboarding copy', status: 'review' },
  { title: 'Fix billing export', status: 'open' },
  { title: 'Ship dark theme', status: 'done' },
  { title: 'Audit color contrast', status: 'review' },
];

const filters = ['open', 'review', 'done'];

Form/Integration

Form/Integration: explicit button types in a search form

ButtonBase renders a native <button>, whose default type is submit. The Clear action sets type='button' so it does not submit the form; Search submits it and the query is read with FormData.

submit-button-with-explicit-type.tsx · 46 lines
import { ButtonBase, TextField, TextFieldInput } from '@vega-ui/react';
import { FormEvent, useState } from 'react';

const actionStyle = {
  flexShrink: 0,
  paddingInline: 'var(--spacing-8)',
  borderRadius: 'var(--radius-3)',
  fontSize: 'var(--font-size-2)',
};

export default function SubmitButtonWithExplicitType() {
  const [query, setQuery] = useState('');

Layout/Overlay

Layout/Overlay: full-row actions in a settings list

ButtonBase does not impose layout, so a whole list row can become one action with an icon, two lines of text, and a chevron. textAlign: 'start' and width: '100%' turn the centered button into a row.

list-row-actions.tsx · 63 lines
import { ButtonBase, Icon } from '@vega-ui/react';
import { BellIcon, ChevronRightIcon, LockIcon, UserIcon } from '@vega-ui/icons';
import { type ReactNode, useState } from 'react';
import './examples.css';

const rows: { id: string, label: string, hint: string, icon: ReactNode }[] = [
  { id: 'profile', label: 'Profile', hint: 'Name, photo, bio', icon: <UserIcon /> },
  { id: 'security', label: 'Security', hint: 'Password and two-factor', icon: <LockIcon /> },
  { id: 'notifications', label: 'Notifications', hint: 'Email and push', icon: <BellIcon /> },
];

export default function ListRowActions() {

Disabled

Disabled: blocked while permissions are loading

The action stays disabled until the permission check resolves. The reason is shown in a status next to the button, because a disabled button cannot be focused to read a description.

blocked-action-while-permissions-are-loading.tsx · 43 lines
import { ButtonBase, Spinner } from '@vega-ui/react';
import { useEffect, useState } from 'react';

export default function BlockedActionWhilePermissionsAreLoading() {
  const [canTransfer, setCanTransfer] = useState<boolean | null>(null);

  useEffect(() => {
    const timer = setTimeout(() => setCanTransfer(true), 2000);
    return () => clearTimeout(timer);
  }, []);

  const loading = canTransfer === null;

Edge

Edge: custom variant string

variant accepts any string, but only primary and secondary have CSS. A custom value such as danger gets no colors until you set the --button-* variables yourself; its disabled look also needs your own CSS.

custom-variant-string.tsx · 31 lines
import type { CSSProperties } from 'react';
import { ButtonBase } from '@vega-ui/react';
import { useState } from 'react';

const danger = {
  '--button-color': 'var(--color-white)',
  '--button-color-hover': 'var(--color-white)',
  '--button-color-active': 'var(--color-white)',
  '--button-background-color': 'var(--color-red-accent-500)',
  '--button-background-color-hover': 'var(--color-red-accent-600)',
  '--button-background-color-active': 'var(--color-red-accent-700)',
  padding: 'var(--spacing-3) var(--spacing-8)',

Anatomy

Overview

ButtonBase is a single-part action primitive. It does not export structural child parts, but it can hand its behavior and styling to another element through asChild.

Required Parts

  • ButtonBase: root interactive element that owns styling, focus ring, disabled behavior, and data attributes

Optional Parts

  • slotted child element via asChild: use when the final semantic element should be a link or router primitive instead of a native button

Composition Order

  1. ButtonBase
  2. optional child element when asChild is enabled

Valid Composition Patterns

  • native button for actions, submits, and resets
  • asChild with one anchor or router link when the action must navigate
  • text-only, icon-only, or mixed content as long as the final accessible name is clear

Invalid Composition Patterns

  • multiple direct children when relying on a single slotted semantic target
  • asChild with a child that does not accept forwarded props
  • treating ButtonBase as a complete product button when Button or IconButton is the clearer public API