Button
A versatile button component with multiple variants, colors, sizes, and states. Includes both standalone buttons and button groups for organizing related actions. Built with accessibility in mind and fully customizable through CSS custom properties.
Features
Button
- ♿ Accessible: Full keyboard support, ARIA attributes, screen reader friendly
- 🌈 6 Semantic Colors: primary, secondary, info, success, warning, error
- 🎨 6 Variants: solid, flat, bordered, outline, ghost, text
- 🎭 States: loading, disabled
- 📏 3 Sizes: sm, md, lg
- 🔗 Link Mode: renders as an accessible
<a role="button">whenhrefis set - 🔧 Customizable: CSS custom properties for styling
Button Group
- 🔄 2 Orientations: horizontal, vertical
- 🔗 Attached Mode: Connect buttons with shared borders
- 📐 Full Width: Buttons expand to fill container
- 📏 Attribute Propagation: Automatically apply size, variant, and color to all children
Source Code
View Source Code
import { define, prop, computed, defineField, effect, html, inject } from '@vielzeug/craftit';
import type { ButtonType, DisablableProps, RoundedSize, SizableProps, ThemableProps, VisualVariant } from '../../types';
import {
disabledLoadingMixin,
forcedColorsMixin,
formFieldMixins,
frostVariantMixin,
rainbowEffectMixin,
sizeVariantMixin,
} from '../../styles';
import { BUTTON_GROUP_CTX } from '../button-group/button-group';
import { disablableBundle, loadableBundle, roundableBundle, sizableBundle, themableBundle } from '../shared/bundles';
import componentStyles from './button.css?inline';
const BUTTON_COLORS = ['primary', 'secondary', 'info', 'success', 'warning', 'error'] as const;
const BUTTON_SIZES = ['sm', 'md', 'lg'] as const;
const BUTTON_VARIANTS = ['solid', 'flat', 'bordered', 'outline', 'ghost', 'text', 'frost'] as const;
/** Button component properties */
export type BitButtonProps = ThemableProps &
SizableProps &
DisablableProps & {
/** Full width button (100% of container) */
fullwidth?: boolean;
/** When set, renders an `<a>` instead of `<button>` */
href?: string;
/** Icon-only mode (square aspect ratio, no padding) */
iconOnly?: boolean;
/** Accessible label for the inner button — required for icon-only buttons */
label?: string;
/** Show loading state with spinner */
loading?: boolean;
/** Enable animated rainbow border effect */
rainbow?: boolean;
/** Link rel attribute (requires href) */
rel?: string;
/** Border radius size */
rounded?: RoundedSize;
/** Link target (requires href) */
target?: '_blank' | '_self' | '_parent' | '_top';
/** HTML button type attribute */
type?: ButtonType;
/** Visual style variant */
variant?: Exclude<VisualVariant, 'glass'>;
};
/**
* A customizable button component with multiple variants, sizes, and states.
* Supports icons, loading states, and special effects like frost and rainbow.
*
* @element bit-button
*
* @attr {string} type - HTML button type: 'button' | 'submit' | 'reset'
* @attr {boolean} disabled - Disable button interaction
* @attr {boolean} loading - Show loading state with spinner
* @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'
* @attr {string} variant - Visual variant: 'solid' | 'flat' | 'bordered' | 'outline' | 'ghost' | 'frost'
* @attr {string} size - Button size: 'sm' | 'md' | 'lg'
* @attr {string} rounded - Border radius: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full'
* @attr {boolean} rainbow - Enable animated rainbow border effect
* @attr {boolean} icon-only - Icon-only mode (square aspect ratio, no padding)
* @attr {boolean} fullwidth - Full width button (100% of container)
*
* @fires click - Emitted when button is clicked (unless disabled/loading)
*
* @slot - Button content (text, icons, etc.)
* @slot prefix - Content before the button text (e.g., icons)
* @slot suffix - Content after the button text (e.g., icons, badges)
*
* @part button - The button element
* @part loader - The loading spinner element
* @part content - The button content wrapper
*
* @cssprop --button-bg - Background color
* @cssprop --button-color - Text color
* @cssprop --button-hover-bg - Hover background
* @cssprop --button-active-bg - Active/pressed background
* @cssprop --button-border - Border width
* @cssprop --button-border-color - Border color
* @cssprop --button-radius - Border radius
* @cssprop --button-padding - Inner padding
* @cssprop --button-gap - Gap between icon and text
* @cssprop --button-font-size - Font size
*
* @example
* ```html
* <bit-button variant="solid" color="primary">Click me</bit-button>
* <bit-button loading color="success">Processing...</bit-button>
* <bit-button variant="frost" rainbow>Special Button</bit-button>
* ```
*/
export const BUTTON_TAG = define<BitButtonProps, { click: MouseEvent }>('bit-button', {
formAssociated: true,
props: {
...themableBundle,
...sizableBundle,
...disablableBundle,
...loadableBundle,
...roundableBundle,
fullwidth: false,
href: undefined,
iconOnly: false,
label: undefined,
rainbow: false,
rel: undefined,
target: undefined,
type: prop.oneOf(['button', 'submit', 'reset'] as const, 'button'),
variant: prop.oneOf(BUTTON_VARIANTS, 'solid'),
},
setup(props, { emit, host }) {
// Reactively inherit size/variant/color from a parent bit-button-group when present.
const groupCtx = inject(BUTTON_GROUP_CTX);
if (groupCtx) {
effect(() => {
const color = groupCtx.color.value;
const size = groupCtx.size.value;
const variant = groupCtx.variant.value;
if (color !== undefined && BUTTON_COLORS.includes(color as (typeof BUTTON_COLORS)[number])) {
props.color.value = color as BitButtonProps['color'];
}
if (size !== undefined && BUTTON_SIZES.includes(size as (typeof BUTTON_SIZES)[number])) {
props.size.value = size as BitButtonProps['size'];
}
if (variant !== undefined && BUTTON_VARIANTS.includes(variant as (typeof BUTTON_VARIANTS)[number])) {
props.variant.value = variant as BitButtonProps['variant'];
}
});
}
const isLink = computed(() => !!props.href.value);
const isDisabled = computed(() => !!(props.disabled.value || props.loading.value));
// Form association: relay submit/reset clicks to the associated form.
// The inner <button> always has type="button" so shadow DOM never drives native form actions.
const formField = defineField({
disabled: isDisabled,
toFormValue: () => null,
value: computed(() => ''),
});
// Unified click handler for both links and buttons.
// Forward only when native click didn't traverse to host (jsdom/shadow interop).
const handleClick = (e: MouseEvent) => {
if (isDisabled.value) {
e.preventDefault();
e.stopImmediatePropagation();
return;
}
// If it's a form button, handle submit/reset
if (isLink.value) return;
const form = formField.internals.form;
if (form) {
if (props.type.value === 'submit') {
e.preventDefault();
form.requestSubmit();
} else if (props.type.value === 'reset') {
e.preventDefault();
form.reset();
}
}
const path = e.composedPath();
const reachedHost = path.includes(host.el);
if (!reachedHost) {
emit('click', e);
}
};
host.bind({
attr: {
'aria-busy': props.loading,
'aria-disabled': isDisabled,
'aria-label': props.label,
},
});
return () => html`
${() =>
isLink.value
? html`<a
part="button"
:href="${props.href}"
:target="${props.target}"
:rel="${props.rel}"
role="button"
:aria-disabled="${() => (isDisabled.value ? 'true' : null)}"
:aria-busy="${() => (props.loading.value ? 'true' : null)}"
@click="${handleClick}">
<span class="loader" part="loader" aria-label="Loading" ?hidden=${() => !props.loading.value}></span>
<slot name="prefix"></slot>
<span class="content" part="content"><slot></slot></span>
<slot name="suffix"></slot>
</a>`
: html`<button
part="button"
type="button"
?disabled=${isDisabled}
:aria-disabled="${() => (isDisabled.value ? 'true' : null)}"
:aria-busy="${() => (props.loading.value ? 'true' : null)}"
@click="${handleClick}">
<span class="loader" part="loader" aria-label="Loading" ?hidden=${() => !props.loading.value}></span>
<slot name="prefix"></slot>
<span class="content" part="content"><slot></slot></span>
<slot name="suffix"></slot>
</button>`}
`;
},
shadow: { delegatesFocus: true },
styles: [
...formFieldMixins,
forcedColorsMixin,
sizeVariantMixin({
lg: {
fontSize: 'var(--text-base)',
gap: 'var(--size-2-5)',
height: 'var(--size-12)',
iconSize: 'var(--size-6)',
lineHeight: 'var(--leading-relaxed)',
padding: 'var(--size-2-5) var(--size-5)',
},
sm: {
fontSize: 'var(--text-sm)',
gap: 'var(--size-1-5)',
height: 'var(--size-8)',
iconSize: 'var(--size-4)',
lineHeight: 'var(--leading-tight)',
padding: 'var(--size-1-5) var(--size-3)',
},
}),
frostVariantMixin('button'),
rainbowEffectMixin('button'),
disabledLoadingMixin('button'),
componentStyles,
],
});View Source Code (Button Group)
import { define, createContext, html, provide, type ReadonlySignal } from '@vielzeug/craftit';
import { sizableBundle, themableBundle } from '../shared/bundles';
import styles from './button-group.css?inline';
/** Button group properties */
export type BitButtonGroupProps = {
/** Join buttons together into a single unit */
attached?: boolean;
/** Theme color tint for all child buttons */
color?: string;
/** Group children span full width */
fullwidth?: boolean;
/** Label for screen readers */
label?: string;
/** Layout direction */
orientation?: 'horizontal' | 'vertical';
/** Shared size for all child buttons */
size?: string;
/** Shared visual variant for all child buttons */
variant?: string;
};
/** Shared context for button groups */
export type ButtonGroupContext = {
color: ReadonlySignal<string | undefined>;
size: ReadonlySignal<string | undefined>;
variant: ReadonlySignal<string | undefined>;
};
export const BUTTON_GROUP_CTX = createContext<ButtonGroupContext | undefined>('BitButtonGroup');
/**
* A container for grouping related buttons.
* Child `bit-button` components automatically inherit the group's color, size, and variant.
*
* @element bit-button-group
*
* @attr {boolean} attached - Join buttons together into a unit
* @attr {string} color - Shared color tint
* @attr {boolean} fullwidth - Group spans full width
* @attr {string} label - Accessible label
* @attr {string} orientation - 'horizontal' | 'vertical'
* @attr {string} size - Shared size
* @attr {string} variant - Shared visual variant
*
* @slot - Place bit-button elements here
*
* @cssprop --button-border-start - Button styling token.
* @cssprop --button-border-top - Button styling token.
* @cssprop --button-radius - Button styling token.
* @cssprop --group-gap - Group layout/styling token.
* @cssprop --group-radius - Group layout/styling token.
* @cssprop --rounded-lg - Border radius token.
* @cssprop --size-2 - Spacing/sizing token.
* @part group - Group container.
* @example
* ```html
* <bit-button-group><bit-button>First</bit-button><bit-button>Second</bit-button></bit-button-group>
* ```
*/
export const BUTTON_GROUP_TAG = define<BitButtonGroupProps>('bit-button-group', {
props: {
...themableBundle,
...sizableBundle,
attached: false,
fullwidth: false,
label: undefined,
orientation: undefined,
variant: undefined,
},
setup(props) {
provide(BUTTON_GROUP_CTX, {
color: props.color!,
size: props.size!,
variant: props.variant!,
});
return () => html`
<div class="button-group" part="group" role="group" :aria-label="${props.label}">
<slot></slot>
</div>
`;
},
styles: [styles],
});Basic Usage
Standalone Button
<bit-button variant="solid" color="primary">Click me</bit-button>
<script type="module">
import '@vielzeug/buildit/button';
</script>Button Group
<bit-button-group>
<bit-button>First</bit-button>
<bit-button>Second</bit-button>
<bit-button>Third</bit-button>
</bit-button-group>
<script type="module">
import '@vielzeug/buildit/button';
import '@vielzeug/buildit/button-group';
</script>Visual Options
Variants
The button comes with eight visual variants to match different levels of emphasis.
Frost Variant
Modern frost effect with backdrop blur that adapts based on color:
- Without color: Subtle canvas-based frost overlay
- With color: Frosted glass effect with colored tint
Best Used With
Frost variant works best when placed over colorful backgrounds or images to showcase the blur and transparency effects.
Rainbow Border
Animated rainbow border effect perfect for highlighting call-to-action buttons or special features.
Colors
Six semantic colors for different contexts.
Sizes
Three sizes for different contexts.
States
Loading
Show a loading spinner and prevent interaction during async operations.
Disabled
Prevent interaction and reduce opacity for unavailable actions.
Icons & Extras
With Icons
Add prefix or suffix icons using slots.
Rounded (Custom Border Radius)
Use the rounded attribute to apply border radius from the theme. Use it without a value (or rounded="full") for pill shape, or specify a theme value like "lg", "xl", etc.
Full Width
Button expands to fill the full width of its container.
Link Buttons
When href is provided, bit-button renders as an <a role="button"> element instead of <button>. All visual variants, sizes, states, and slots behave exactly the same.
Security
Always set rel="noopener noreferrer" when using target="_blank" to prevent tabnapping attacks.
Button Groups
Orientation
Group buttons in horizontal or vertical layouts.
Horizontal (Default)
Vertical
Attached Mode
Remove spacing and connect buttons with shared borders for segmented controls.
Attribute Propagation
Apply size, variant, or color to all child buttons automatically via the parent group.
Full Width
Buttons expand to fill the container equally.
API Reference
bit-button Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
variant | 'solid' | 'flat' | 'bordered' | 'outline' | 'ghost' | 'text' | 'frost' | 'solid' | Visual style variant |
color | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'primary' | Semantic color |
size | 'sm' | 'md' | 'lg' | 'md' | Button size |
type | 'button' | 'submit' | 'reset' | 'button' | Button type (for forms) |
disabled | boolean | false | Disable the button |
loading | boolean | false | Show loading state |
rainbow | boolean | false | Animated rainbow border effect |
icon-only | boolean | false | Icon-only mode (square aspect ratio, no padding) |
label | string | — | Accessible label for the inner element — required for icon-only buttons |
fullwidth | boolean | false | Button takes full width of container |
rounded | boolean | false | Fully rounded corners |
href | string | — | URL to navigate to; renders as <a role="button"> |
target | '_blank' | '_self' | '_parent' | '_top' | — | Link target (requires href) |
rel | string | — | Link rel attribute (requires href) |
bit-button-group Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
orientation | 'horizontal' | 'vertical' | 'horizontal' | Group layout direction |
attached | boolean | false | Remove spacing and connect buttons |
fullwidth | boolean | false | Buttons expand to fill container |
size | 'sm' | 'md' | 'lg' | - | Apply size to all child buttons |
variant | 'solid' | 'flat' | 'bordered' | 'outline' | 'ghost' | 'text' | 'frost' | - | Apply variant to all child buttons |
color | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | - | Apply color to all child buttons |
Slots
bit-button
| Slot | Description |
|---|---|
| (default) | Button content (text, icons, etc.) |
prefix | Content before the main content |
suffix | Content after the main content |
bit-button-group
| Slot | Description |
|---|---|
| (default) | Child button elements |
Events
bit-button
| Event | Detail | Description |
|---|---|---|
click | MouseEvent | Native click event — standard MouseEvent, not suppressed |
bit-button-group
No events.
CSS Custom Properties
bit-button
| Property | Description | Default |
|---|---|---|
--button-bg | Background color | Variant-dependent |
--button-color | Text color | Variant-dependent |
--button-radius | Border radius | 0.375rem |
--button-padding | Inner padding | Size-dependent |
bit-button-group
| Property | Description | Default |
|---|---|---|
--group-gap | Spacing between buttons | 0.5rem |
--group-radius | Border radius for first/last buttons in attached mode | 0.375rem |
Accessibility
Both components follow WAI-ARIA best practices.
bit-button
✅ Keyboard Navigation
EnterandSpaceactivate the button.Tabmoves focus to/from the button.
✅ Screen Readers
- Announces button role and label.
aria-disabledwhen disabled.aria-busywhen loading.- Icon-only buttons require
labelattribute.
bit-button-group
✅ Semantic Structure
- Automatically includes
role="group"on the container. - Use the
labelattribute to provide context for screen readers (e.g.,label="Text alignment").
✅ Keyboard Navigation
Tabmoves focus between buttons.
✅ Screen Readers
- Buttons within a group are announced in context.
Best Practices
bit-button
Do:
- Use semantic colors to communicate intent.
- Provide
aria-labelfor icon-only buttons. - Use loading state for async operations.
Don't:
- Use multiple primary buttons in the same context.
- Nest interactive elements inside buttons.
bit-button-group
Do:
- Use
attachedmode for related segmented controls. - Use
fullwidthfor mobile-optimized layouts or primary actions. - Provide an
aria-labelwhen the group's purpose isn't clear from the content.
Don't:
- Mix too many variants or colors within a single group.
- Use
verticalorientation for more than 4-5 buttons if possible.