Button Set
Free · MITOne button primitive covering primary/secondary/ghost/destructive variants, three sizes, a loading spinner state, and an icon-only mode.
- Category
- Core UI
- Framework
- astro
- Version
- 1.0.0
- Peer deps
- none
Install
velocityx add button-set
Free and MIT licensed — no account needed. The file lands under src/components/velocity/button-set/ by default (override with --dest), and it's yours to edit from the moment it's written.
Demo
View the live demo on the showcase site ↗
Source
Pulled live from the registry — this is the exact file velocityx add button-set writes into your project right now.
components/velocity/button-set/ButtonSet.astro
---
// VELOCITY COMPONENT · theming
//
// Colours here are Tailwind `primary-*` / `accent-*`, which resolve through
// the Velocity preset. Out of the box they are a neutral indigo — NOT the
// Aidxn brand — so this drops into your project without dragging our look in.
//
// ⚡ NOT USING VELOCITY? Run this once, in your project root:
//
// velocityx theme
//
// It writes velocity-preset.cjs + velocity-theme.css. Add the preset to your
// tailwind.config, import the css, then set your whole brand in ONE place:
//
// :root {
// --vx-brand: custom;
// --vx-primary-600: 220 38 38; <- your action colour
// --vx-primary-700: 185 28 28;
// }
//
// ⚠️ Raw RGB channels, space separated — NOT hex. `#DC2626` here silently
// breaks every /opacity variant in the library (bg-primary-600/20 etc).
//
// Prefer not to install anything? Find-and-replace `primary-` with your own
// scale name. Nothing else in this file depends on Velocity.
/**
* ButtonSet — the single button primitive covering every state the library
* needs: primary/secondary/ghost/destructive variants, sm/md/lg sizes,
* loading (spinner replaces label, button stays the same width via
* aria-busy + disabled), and icon-only (square, requires aria-label).
*
* Renders an <a> when `href` is passed, a <button> otherwise — same visual
* system either way.
*
* Usage:
* <ButtonSet variant="primary">Save changes</ButtonSet>
* <ButtonSet variant="destructive" size="sm">Delete</ButtonSet>
* <ButtonSet variant="secondary" loading>Saving…</ButtonSet>
* <ButtonSet variant="ghost" icon aria-label="Settings"><svg .../></ButtonSet>
*/
export interface Props {
variant?: 'primary' | 'secondary' | 'ghost' | 'destructive';
size?: 'sm' | 'md' | 'lg';
href?: string;
type?: 'button' | 'submit' | 'reset';
loading?: boolean;
disabled?: boolean;
icon?: boolean;
'aria-label'?: string;
class?: string;
}
const {
variant = 'primary',
size = 'md',
href,
type = 'button',
loading = false,
disabled = false,
icon = false,
class: className = '',
...rest
} = Astro.props;
const VARIANTS = {
primary: 'bg-primary-600 text-white hover:bg-primary-500 focus-visible:ring-primary-600 disabled:bg-primary-300 dark:disabled:bg-primary-900',
secondary: 'bg-white text-secondary-800 border border-secondary-300 hover:bg-secondary-50 dark:bg-secondary-900 dark:text-secondary-200 dark:border-secondary-700 dark:hover:bg-secondary-800 focus-visible:ring-primary-600',
ghost: 'bg-transparent text-secondary-700 hover:bg-secondary-100 dark:text-secondary-300 dark:hover:bg-secondary-800 focus-visible:ring-primary-600',
destructive: 'bg-red-600 text-white hover:bg-red-500 focus-visible:ring-red-600 disabled:bg-red-300',
} as const;
const SIZES = {
sm: icon ? 'h-8 w-8' : 'h-8 px-3 text-xs gap-1.5',
md: icon ? 'h-10 w-10' : 'h-10 px-4 text-sm gap-2',
lg: icon ? 'h-12 w-12' : 'h-12 px-6 text-base gap-2',
} as const;
const Tag = href ? 'a' : 'button';
const isDisabled = disabled || loading;
---
<Tag
href={href}
type={!href ? type : undefined}
disabled={!href && isDisabled ? true : undefined}
aria-disabled={href && isDisabled ? 'true' : undefined}
aria-busy={loading ? 'true' : undefined}
tabindex={href && isDisabled ? -1 : undefined}
class:list={[
'inline-flex items-center justify-center rounded-xl font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-secondary-950 disabled:cursor-not-allowed disabled:opacity-70',
VARIANTS[variant],
SIZES[size],
className,
]}
{...rest}
>
{loading && (
<svg aria-hidden="true" class="h-4 w-4 animate-spin motion-reduce:animate-none" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="9" stroke="currentColor" stroke-width="3" />
<path class="opacity-90" d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" stroke-width="3" stroke-linecap="round" />
</svg>
)}
{!loading && <slot />}
{loading && !icon && <span class="sr-only">Loading</span>}
</Tag>
Theming
Uses the shared primary-* / accent-* scale — see Theming if colours look unstyled after install.