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 { computed, defineComponent, defineField, fire, html, inject, syncContextProps } from '@vielzeug/craftit';
import { when } from '@vielzeug/craftit/directives';
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 componentStyles from './button.css?inline';
/** 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 = defineComponent<BitButtonProps>({
formAssociated: true,
props: {
color: { default: undefined },
disabled: { default: false },
fullwidth: { default: false },
href: { default: undefined },
iconOnly: { default: false },
label: { default: undefined },
loading: { default: false },
rainbow: { default: false },
rel: { default: undefined },
rounded: { default: undefined },
size: { default: undefined },
target: { default: undefined },
type: { default: 'button' },
variant: { default: 'solid' },
},
setup({ host, props }) {
// Reactively inherit size/variant/color from a parent bit-button-group when present.
syncContextProps(inject(BUTTON_GROUP_CTX, undefined), props, ['color', 'size', 'variant']);
const isDisabled = computed(() => props.disabled.value || props.loading.value || false);
const isLink = computed(() => !!props.href.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(() => ''),
});
// Prevent navigation on disabled links; native <button disabled> handles the button case.
const handleLinkClick = (e: MouseEvent) => {
if (isDisabled.value) {
e.preventDefault();
e.stopPropagation();
return;
}
// Relay to host as a proper MouseEvent. stopPropagation prevents double-dispatch in
// browsers where shadow DOM already retargets the event to the host.
e.stopPropagation();
fire.mouse(host, e.type, e);
};
const handleButtonClick = (e: MouseEvent) => {
if (isDisabled.value) return;
// Relay to host, handle form submission, and stop inner bubble in one place.
e.stopPropagation();
const form = formField.internals.form;
if (form) {
if (props.type.value === 'submit') form.requestSubmit();
else if (props.type.value === 'reset') form.reset();
}
fire.mouse(host, e.type, e);
};
return html`
${when(
isLink,
() =>
html`<a
part="button"
:href="${() => props.href.value ?? null}"
:target="${() => props.target.value ?? null}"
:rel="${() => props.rel.value ?? null}"
:aria-label="${() => props.label.value ?? null}"
:aria-disabled="${() => (isDisabled.value ? 'true' : null)}"
:aria-busy="${() => (props.loading.value ? 'true' : null)}"
role="button"
@click="${handleLinkClick}">
<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-label="${() => props.label.value ?? null}"
:aria-disabled="${() => (isDisabled.value ? 'true' : null)}"
:aria-busy="${() => (props.loading.value ? 'true' : null)}"
@click="${handleButtonClick}">
<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,
],
tag: 'bit-button',
});View Source Code (Button Group)
import { createContext, defineComponent, html, provide, type ReadonlySignal } from '@vielzeug/craftit';
import type { ComponentSize, ThemeColor, VisualVariant } from '../../types';
/** Context provided by bit-button-group to its bit-button children. */
export type ButtonGroupContext = {
color: ReadonlySignal<ThemeColor | undefined>;
size: ReadonlySignal<ComponentSize | undefined>;
variant: ReadonlySignal<Exclude<VisualVariant, 'glass'> | undefined>;
};
/** Injection key for the button-group context. */
export const BUTTON_GROUP_CTX = createContext<ButtonGroupContext>('ButtonGroupContext');
import styles from './button-group.css?inline';
/** Button group component properties */
export type BitButtonGroupProps = {
/** Attach buttons together (no gap, rounded only on edges) */
attached?: boolean;
/** Button color for all children (propagated) */
color?: ThemeColor;
/** Make all buttons full width */
fullwidth?: boolean;
/** Accessible group label */
label?: string;
/** Group orientation */
orientation?: 'horizontal' | 'vertical';
/** Button size for all children (propagated) */
size?: ComponentSize;
/** Button variant for all children (propagated) */
variant?: Exclude<VisualVariant, 'glass'>;
};
// -------------------- Component Definition --------------------
/**
* A container for grouping buttons with coordinated styling and layout.
*
* @element bit-button-group
*
* @attr {string} size - Button size: 'sm' | 'md' | 'lg'
* @attr {string} variant - Visual variant: 'solid' | 'flat' | 'bordered' | 'outline' | 'ghost' | 'frost'
* @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'
* @attr {string} orientation - Group orientation: 'horizontal' | 'vertical'
* @attr {boolean} attached - Attach buttons together (no gap, rounded only on edges)
* @attr {boolean} fullwidth - Make all buttons full width
* @attr {string} label - Accessible group label
*
* @slot - Button elements (bit-button)
*
* @cssprop --group-gap - Gap between buttons
* @cssprop --group-radius - Border radius for edge buttons
*
* @example
* ```html
* <bit-button-group><bit-button>First</bit-button><bit-button>Second</bit-button></bit-button-group>
* ```
*/
export const BUTTON_GROUP_TAG = defineComponent<BitButtonGroupProps>({
props: {
attached: { default: false },
color: { default: undefined },
fullwidth: { default: false },
label: { default: undefined },
orientation: { default: undefined },
size: { default: undefined },
variant: { default: 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.value ?? null}">
<slot></slot>
</div>
`;
},
styles: [styles],
tag: 'bit-button-group',
});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.