Button
Trigger actions and navigate. Composes variant (solid/outline/ghost/soft/link) with tone (neutral/primary/success/warning/danger/info).
Overview
The Button component is a core building block of the Byteion design system, providing a robust, accessibility-compliant interface for its specific UI capabilities.
Why this exists
Buttons are the most frequent interactive element in any interface. A well-designed button system communicates hierarchy, state, and intent at a glance. Byteion buttons bundle tactile feedback (micro-animations, shadow transitions), full keyboard/ARIA support, and consistent disabled/loading states so you never ship an ambiguous call-to-action.
Mental model
Think of a button as a semantic contract: <button> for actions, <a> styled as a button for navigation. The variant you pick signals priority — only one primary action should exist per section.
When to use
When not to use
Basic example
The following example demonstrates standard usage, fully compliant with Byteion's token-first approach.
Solid variants
Outline variants
Soft variants
Ghost variants
Link variants
Sizes and shapes
Icons and icon-only buttons
Loading and disabled states
Button groups
Usage guidelines
Use one primary action per region. Use secondary for neutral supporting actions, not a brand accent. Use button elements for actions and anchors for navigation. Keep visible focus rings and include aria-label on icon-only buttons.
Free version includes
Solid, outline, soft, ghost, link, semantic intents, xs-xl sizes, icons, loading, disabled, focus states, button groups, and segmented groups.
Pro or future patterns
Advanced gradient CTA buttons, animated buttons, sticky action bars, command actions, analytics tracking, and richer split-button menus.
import { Button } from '@byteion/react';
export default function ButtonDemo() {
return (
<div className="space-y-6 p-6">
{/* Solid */}
<div className="flex flex-wrap items-center gap-3">
<Button variant="solid" tone="primary">Primary</Button>
<Button variant="solid" tone="neutral">Secondary</Button>
<Button variant="solid" tone="success">Success</Button>
<Button variant="solid" tone="warning">Warning</Button>
<Button variant="solid" tone="danger">Danger</Button>
<Button variant="ghost">Ghost</Button>
</div>
{/* Outline */}
<div className="flex flex-wrap items-center gap-3">
<Button variant="outline" tone="primary">Primary</Button>
<Button variant="outline" tone="success">Success</Button>
<Button variant="outline" tone="danger">Danger</Button>
<Button variant="outline" tone="neutral">Neutral</Button>
</div>
{/* Soft */}
<div className="flex flex-wrap items-center gap-3">
<Button variant="soft" tone="primary">Primary</Button>
<Button variant="soft" tone="success">Success</Button>
<Button variant="soft" tone="danger">Danger</Button>
<Button variant="soft" tone="warning">Warning</Button>
</div>
{/* Sizes */}
<div className="flex flex-wrap items-center gap-3">
<Button size="xs">XS</Button>
<Button size="sm">Small</Button>
<Button size="md">Default</Button>
<Button size="lg">Large</Button>
</div>
{/* Icon-only */}
<div className="flex flex-wrap items-center gap-3">
<Button aria-label="Add" iconOnly><i className="ti ti-plus text-lg" /></Button>
<Button aria-label="Edit" iconOnly variant="outline"><i className="ti ti-edit text-lg" /></Button>
<Button aria-label="Delete" iconOnly variant="soft" tone="danger"><i className="ti ti-trash text-lg" /></Button>
</div>
{/* States */}
<div className="flex flex-wrap items-center gap-3">
<Button disabled>Disabled</Button>
<Button loading>Saving…</Button>
<Button tone="success"><i className="ti ti-check text-base" /> Saved</Button>
</div>
</div>
);
}
<script setup lang="ts">
import { Button } from '@byteion/vue';
</script>
<template>
<div class="space-y-6 p-6">
<!-- Solid -->
<div class="flex flex-wrap items-center gap-3">
<Button variant="solid" tone="primary">Primary</Button>
<Button variant="solid" tone="neutral">Secondary</Button>
<Button variant="solid" tone="success">Success</Button>
<Button variant="solid" tone="warning">Warning</Button>
<Button variant="solid" tone="danger">Danger</Button>
<Button variant="ghost">Ghost</Button>
</div>
<!-- Outline -->
<div class="flex flex-wrap items-center gap-3">
<Button variant="outline" tone="primary">Primary</Button>
<Button variant="outline" tone="success">Success</Button>
<Button variant="outline" tone="danger">Danger</Button>
<Button variant="outline" tone="neutral">Neutral</Button>
</div>
<!-- Soft -->
<div class="flex flex-wrap items-center gap-3">
<Button variant="soft" tone="primary">Primary</Button>
<Button variant="soft" tone="success">Success</Button>
<Button variant="soft" tone="danger">Danger</Button>
<Button variant="soft" tone="warning">Warning</Button>
</div>
<!-- Sizes -->
<div class="flex flex-wrap items-center gap-3">
<Button size="xs">XS</Button>
<Button size="sm">Small</Button>
<Button size="md">Default</Button>
<Button size="lg">Large</Button>
</div>
<!-- Icon-only -->
<div class="flex flex-wrap items-center gap-3">
<Button aria-label="Add" icon-only><i class="ti ti-plus text-lg" /></Button>
<Button aria-label="Edit" icon-only variant="outline"><i class="ti ti-edit text-lg" /></Button>
<Button aria-label="Delete" icon-only variant="soft" tone="danger"><i class="ti ti-trash text-lg" /></Button>
</div>
<!-- States -->
<div class="flex flex-wrap items-center gap-3">
<Button disabled>Disabled</Button>
<Button loading>Saving…</Button>
<Button tone="success"><i class="ti ti-check text-base" /> Saved</Button>
</div>
</div>
</template>
Variants
Each variant communicates a different meaning. Choose based on priority, not aesthetics.
- solid — solid style variant.
- outline — outline style variant.
- ghost — Minimal emphasis — cancel, back, optional paths.
- soft — soft style variant.
- link — link style variant.
Sizes
Size should match the importance and context of the component.
- xs — Compact — icon-only toolbars, dense tables.
- sm — Small — secondary actions, inline forms.
- md — Medium — standard for most use cases.
- lg — Large — hero CTAs, mobile touch targets.
- xl — xl size.
States
This component reacts seamlessly to the following interaction and data states:
- default — Resting appearance before interaction.
- hover — Pointer is over the element — visual feedback via color or shadow.
- focus-visible — focus-visible state.
- active — Currently selected or in use.
- disabled — Non-interactive — grayed out, cursor not-allowed.
- loading — Action in progress — spinner replaces icon, button is disabled.
Anatomy
aria-label
• Spinner — replaces icon during loading; text remains to preserve button width
• Group wrapper — overflow-hidden rounded border for segmented controlsUsage guidelines
- Use exactly one primary button per section or modal.
- Label should be an action verb, not a noun (Save > Form).
- Place destructive actions on the right or below the form, never as the primary CTA.
- Keep icon buttons within toolbars — do not scatter them across page content.
- Button groups should only contain mutually exclusive options.
Do and Don't
Do
- Use primary for the single most important action.
- Use outline or ghost for secondary and cancel actions.
- Show a loading spinner and disable the button on submit.
- Wrap icon-only buttons in a consistent container (toolbar, table actions).
Don't
- Do not use more than one primary button in a modal footer.
- Do not use gradient buttons for destructive or routine actions.
- Do not hide the button label — icons alone are ambiguous.
- Do not change button height within a form row.
Accessibility
Buttons are natively keyboard-activatable (Enter and Space). Byteion buttons add focus-visible:ring for visible focus outlines. Loading buttons set aria-busy="true" and disabled. Icon-only buttons must have aria-label. Button groups use aria-pressed for the active segment.
Role:button
ARIA Attributes
aria-disabledaria-pressedaria-busyaria-label
Keyboard Navigation
- Enter: Activates the button
- Space: Activates the button
Customization
Change the primary color globally by updating --primary in variables.css. Adjust border radius by modifying the rounded-lg class or the token --radius-button. Change heights by targeting the size-specific classes (h-7, h-8, etc.).
Compose with React cva()
Use the exportedcva() +cn() utilities to compose your own variant on top of the Byteion tokens.
import { cva, cn, type VariantProps } from '@byteion/react';
const myButton = cva('base classes…', {
variants: {
accent: { soft:'border-primary-200', loud:'border-primary-500' },
},
defaultVariants: { accent:'soft' },
});
type Props = React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof myButton>;
Compose with Vue cva()
Use the exportedcva() +cn() utilities inside standard Vue setup blocks.
<script setup lang="ts">
import { computed, useAttrs } from 'vue';
import { cva, type VariantProps } from '@byteion/vue';
interface Props { accent?:'soft' |'loud' }
const props = withDefaults(defineProps<Props>(), { accent:'soft' });
const attrs = useAttrs();
const myButton = cva('base classes…', {
variants: { accent: { soft:'border-primary-200', loud:'border-primary-500' } },
});
const cls = computed(() => myButton({ accent: props.accent, class: attrs.class as string | undefined }));
</script>
Runtime events
Byteion keeps framework APIs idiomatic: HTML uses DOM custom events, React uses callback props, and Vue uses emitted events / v-model updates.
HTML / JS events
No HTML runtime events are declared by this adapter.
React callbacks
No React callback events are declared by this adapter.
Vue emits
No Vue emitted events are declared by this adapter.
Remote / AJAX
This component does not implement Byteion JS remote loading. Use normal HTML form submission, framework state, or your application data layer around it.
API Reference
No data attributes required for static buttons. For JS-driven loading/toggling, use data-bi-component="button". Interactive states are handled via Tailwind pseudo-classes (:hover, :focus-visible, :active, :disabled).
HTML Data Attributes
No custom data attributes required.
React Props · ButtonProps
Extracted directly fromreact/src/components/Button.tsx.
export interface ButtonProps
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'>,
ButtonVariantProps {
leadingIcon?: ReactNode;
trailingIcon?: ReactNode;
children?: ReactNode;
'aria-label'?: string;
/** Removes text padding and applies square/circle geometry. */
iconOnly?: boolean;
/** Screen-reader fallback used when an icon-only button is loading. */
loadingLabel?: string;
}
React Variant Schema
Internal variant composition defined viacva().
cva(
'inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap font-medium transition-all duration-200 ease-out-soft outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-background motion-reduce:transform-none motion-reduce:transition-none disabled:pointer-events-none disabled:translate-y-0 disabled:scale-100 disabled:cursor-not-allowed disabled:opacity-60 disabled:shadow-none',
{
variants: {
variant: {
solid: 'active:scale-[0.98]',
outline: 'border bg-transparent active:scale-[0.98]',
soft: 'shadow-none active:scale-[0.98]',
ghost: 'bg-transparent shadow-none active:scale-[0.98]',
link: 'bg-transparent px-0 shadow-none underline-offset-4 hover:underline active:opacity-80',
},
tone: {
neutral: '',
primary: '',
secondary: '',
success: '',
warning: '',
danger: '',
destructive: '',
info: '',
},
size: {
xs: 'h-7 rounded-md px-2.5 text-xs gap-1.5',
sm: 'h-8 rounded-md px-3 text-xs',
default: 'h-10 rounded-lg px-4 text-sm',
md: 'rounded-lg px-4 py-2 text-sm',
lg: 'rounded-lg px-5 py-2.5 text-sm gap-2.5',
xl: 'rounded-lg px-6 py-3 text-base gap-2.5',
},
shape: {
default: '',
pill: 'rounded-full',
square: 'p-0',
circle: 'rounded-full p-0',
},
block: {
true: 'w-full',
false: '',
},
loading: {
true: 'cursor-progress opacity-90',
false: '',
},
},
compoundVariants: [
{ size: ['xs', 'sm', 'md', 'default', 'lg', 'xl'], shape: ['square', 'circle'], class: 'aspect-square' },
{ size: 'xs', shape: ['square', 'circle'], class: '!h-7 !w-7' },
{ size: 'sm', shape: ['square', 'circle'], class: '!h-8 !w-8' },
{ size: ['md', 'default'], shape: ['square', 'circle'], class: '!h-10 !w-10' },
{ size: 'lg', shape: ['square', 'circle'], class: '!h-11 !w-11' },
{ size: 'xl', shape: ['square', 'circle'], class: '!h-12 !w-12' },
{ variant: 'solid', tone: 'neutral', class: 'bg-neutral-900 text-white shadow-button hover:-translate-y-px hover:bg-neutral-800 hover:shadow-button-hover active:translate-y-0 active:shadow-button focus-visible:ri
// … truncated
Vue Props · ButtonProps
Extracted directly fromvue/src/components/Button.vue.
export interface ButtonProps {
variant?: 'solid' | 'outline' | 'soft' | 'ghost' | 'link';
tone?: 'neutral' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'destructive' | 'info';
size?: 'xs' | 'sm' | 'default' | 'md' | 'lg' | 'xl';
shape?: 'default' | 'pill' | 'square' | 'circle';
block?: boolean;
loading?: boolean;
disabled?: boolean;
iconOnly?: boolean;
loadingLabel?: string;
/** `type` attribute on the underlying `<button>`. Defaults to `'button'`. */
type?: 'button' | 'submit' | 'reset';
}
Real-world examples
See how this component composes in common application contexts.
In a form
In a modal
In a toolbar
Async / Loading state
Developer tips
div with onClick instead of <button> — you lose keyboard and screen-reader support.
• Edge case: when a form has both Save and Delete, make Save primary and Delete ghost/danger — never two primaries.
• Best practice: keep button labels under 3 words. If the label is long, the action is probably ambiguous.Common mistakes
Production usage tips
data-bi-* attributes instead of styling hooks.
Premium examples
Advanced patterns available in Byteion Pro:
Related components
Unlock Premium Patterns
Get access to copy-paste ready Pro blocks, SaaS templates, and advanced data-tables built with this component.
Explore Pro Components