Button
A polymorphic button with six variants, eleven color schemes, icon slots and built-in loading states.
"use client";
import { Button, Icon, HStack } from "astralis-ui";
import { Sparkles } from "lucide-react";
export function ButtonDemo() {
return (
<HStack gap="3" wrap="wrap" justifyContent="center">
<Button leftIcon={<Icon as={Sparkles} size="xs" />}>Get started</Button>
<Button variant="subtle">Learn more</Button>
<Button variant="outline" colorScheme="gray">
Cancel
</Button>
</HStack>
);
}Label
variant
colorScheme
size
rounded
loaderPlacement
import { Button } from "astralis-ui";
<Button>Button</Button>
Import#
import { Button } from "astralis-ui";Usage#
The default button is solid in the brand color at size md. Every visual
decision (variant, hue, size, radius) is one prop away.
import { Button, HStack } from "astralis-ui";
export function ButtonVariants() {
return (
<HStack gap="3" wrap="wrap" justifyContent="center">
<Button variant="solid">Solid</Button>
<Button variant="subtle">Subtle</Button>
<Button variant="surface">Surface</Button>
<Button variant="outline">Outline</Button>
<Button variant="text">Text</Button>
<Button variant="link">Link</Button>
</HStack>
);
}- solid: the signature fill. Use it for the single primary action on a screen.
- subtle: a tinted fill with no border, for secondary actions.
- surface: the bordered sibling of
subtle. - outline: border only; fills with a faint tint on hover.
- text: no chrome until hover (a ghost button).
- link: an inline-link affordance with an underline on hover.
Color schemes#
Every variant paints through the accent channel, so a single colorScheme
prop recolors any button, including runtime brand colors.
Use gray for neutral actions and red for destructive ones. The full
matrix below shows all six variants across all fifteen schemes.
import { Button, Text, VStack, HStack } from "astralis-ui";
const variants = ["solid", "subtle", "surface", "outline", "text", "link"] as const;
const schemes = [
"brand", "gray", "red", "orange", "yellow", "green",
"teal", "blue", "cyan", "purple", "pink",
] as const;
export function ButtonColorSchemes() {
return (
<VStack gap="6" alignItems="stretch">
{variants.map((variant) => (
<VStack key={variant} gap="2" alignItems="stretch">
<Text as="span" size="xs" weight="medium" color="muted">
{variant}
</Text>
<HStack gap="2" wrap="wrap">
{schemes.map((scheme) => (
<Button key={scheme} size="xs" variant={variant} colorScheme={scheme}>
{scheme}
</Button>
))}
</HStack>
</VStack>
))}
</VStack>
);
}Sizes#
Five sizes, from compact toolbars to hero calls-to-action. Height, padding, font size and icon gap scale together.
import { Button, HStack } from "astralis-ui";
export function ButtonSizes() {
return (
<HStack gap="3" wrap="wrap" justifyContent="center">
<Button size="xs">Extra small</Button>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra large</Button>
</HStack>
);
}Rounded#
The corner radius is independent of size: go from sharp to pill with the
rounded prop.
import { Button, HStack } from "astralis-ui";
export function ButtonRounded() {
return (
<HStack gap="3" wrap="wrap" justifyContent="center">
<Button variant="surface" rounded="none">None</Button>
<Button variant="surface" rounded="sm">Small</Button>
<Button variant="surface" rounded="lg">Large</Button>
<Button variant="surface" rounded="2xl">2XL</Button>
<Button variant="surface" rounded="full">Full</Button>
</HStack>
);
}Icons#
Pass any element to leftIcon or rightIcon. Omit the label and the button
becomes a perfect square. Icon-only buttons must carry an aria-label
(the library warns in development if one is missing).
"use client";
import { Button, Icon, HStack } from "astralis-ui";
import { Search, ChevronDown, Settings, Trash2 } from "lucide-react";
export function ButtonIcons() {
return (
<HStack gap="3" wrap="wrap" justifyContent="center">
<Button leftIcon={<Icon as={Search} size="xs" />}>Search</Button>
<Button variant="outline" rightIcon={<Icon as={ChevronDown} size="xs" />}>
Open menu
</Button>
{/* Icon-only buttons need an aria-label for assistive tech. */}
<Button variant="subtle" leftIcon={<Icon as={Settings} size="xs" />} aria-label="Settings" />
<Button
variant="subtle"
colorScheme="red"
leftIcon={<Icon as={Trash2} size="xs" />}
aria-label="Delete"
/>
</HStack>
);
}Loading state#
loading swaps in a spinner and blocks interaction without a layout shift.
loadingText replaces the label while pending, and loaderPlacement moves
the spinner to either side. Bring your own spinner via loader.
"use client";
import { useState } from "react";
import { Button, HStack } from "astralis-ui";
export function ButtonLoading() {
const [saving, setSaving] = useState(false);
const save = () => {
setSaving(true);
setTimeout(() => setSaving(false), 2000);
};
return (
<HStack gap="3" wrap="wrap" justifyContent="center">
<Button loading>Loading</Button>
<Button variant="outline" loading loadingText="Submitting…" />
<Button variant="subtle" loading loaderPlacement="end" loadingText="Uploading" />
<Button onClick={save} loading={saving} loadingText="Saving…">
Click to save
</Button>
</HStack>
);
}Full width#
fullWidth stretches the button to its container, typical for checkout
flows and dialog footers.
"use client";
import { Button, Icon, VStack } from "astralis-ui";
import { CreditCard } from "lucide-react";
export function ButtonFullWidth() {
return (
<VStack gap="3" w="full" maxW="sm">
<Button fullWidth leftIcon={<Icon as={CreditCard} size="xs" />}>
Complete checkout
</Button>
<Button fullWidth variant="outline" colorScheme="gray">
Continue shopping
</Button>
</VStack>
);
}Rendering as a link#
Button is polymorphic: pass as to change the underlying element and the
accepted props follow along type-safely. Disabled non-button elements get
aria-disabled and leave the tab order instead of the disabled attribute.
"use client";
import { Button, Icon, HStack } from "astralis-ui";
import { ExternalLink } from "lucide-react";
export function ButtonAsLink() {
return (
<HStack gap="3" wrap="wrap" justifyContent="center">
{/* Renders a real <a>; href/target come from the anchor's prop types. */}
<Button
as="a"
href="https://github.com"
target="_blank"
variant="surface"
rightIcon={<Icon as={ExternalLink} size="xs" />}
>
View on GitHub
</Button>
<Button as="a" href="#usage" variant="link">
Jump to usage
</Button>
</HStack>
);
}Buttons compose with a surrounding Button Group,
which supplies shared variant, colorScheme, size and disabled defaults.
An explicit prop on a child always wins.
Props#
Button also accepts every prop of the element it renders as (button by default).
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "solid" | "subtle" | "surface" | "outline" | "text" | "link" | "solid" | Visual style. surface is the bordered sibling of subtle; text is the ghost style. |
colorScheme | all 15 schemes | "brand" | Hue the variant paints with. Use gray for a neutral button. |
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Height, padding, font size and icon gap scale together. |
rounded | "none" | "sm" | "md" | "lg" | "xl" | "2xl" | "full" | "lg" | Corner radius. |
fullWidth | boolean | false | Stretches the button to fill its container. |
disabled | boolean | false | Disables the button. On non-button elements this maps to aria-disabled and removes it from the tab order. |
loading | boolean | false | Shows a spinner and disables interaction. |
loadingText | ReactNode | None | Replaces the label while loading (e.g. “Saving…”); the spinner still shows. |
loaderPlacement | "start" | "end" | "start" | Which side the spinner renders on. |
loader | ReactNode | None | Custom spinner element, replacing the built-in one. |
leftIcon | ReactNode | None | Icon rendered before the label. With no label the button becomes icon-only. |
rightIcon | ReactNode | None | Icon rendered after the label. |
as | ElementType | "button" | Element or component to render. Forwarded HTML props follow the chosen element automatically. |
Accessibility#
- Renders a native
<button>by default, so keyboard and screen-reader semantics come for free. - Icon-only buttons need an
aria-label; a development-mode warning fires when it's missing. disabledandloadingon non-button elements map toaria-disabledandtabIndex={-1}rather than the invaliddisabledattribute.- Focus is always visible: a two-pixel outline in the active color scheme's ring token, offset from the button edge.