Askr
UI & Components

Button and Button Group

Use button and button group with the current published Askr packages.

  • @askrjs/ui/button0.0.13
  • @askrjs/themes/button0.0.13
  • @askrjs/themes/button-group0.0.13

How to use button and button group

Button and Button Group should use the standard themed surface unless the application is deliberately implementing its own design system on top of @askrjs/ui.

  1. Import the themed component from @askrjs/themes/components; use @askrjs/ui directly only when building a custom visual system.
  2. Keep button and button group labels, state, and application actions visible at the call site instead of styling internal DOM nodes.
  3. Verify keyboard behavior, focus movement or return, disabled state, and both standard themes.
import { Button, ButtonGroup } from '@askrjs/themes/components';

<ButtonGroup>
  <Button variant="outline" type="button">Cancel</Button>
  <Button type="submit">Save project</Button>
</ButtonGroup>

Purpose

Button is the headless primitive for anything a user presses: it wraps a native <button>, defaults to type="button" so it never triggers an accidental form submit, and delegates all interaction handling to the pressable foundation instead of implementing its own click logic. ButtonGroup, which only exists in @askrjs/themes, clusters related buttons together with shared spacing and an attached/detached border treatment. Reach for Button whenever you need a press target; reach for ButtonGroup when you're laying out two or more buttons as one visual unit, like a toolbar or a segmented set of actions.

Install and import

@askrjs/ui and @askrjs/askr provide the headless Button; add @askrjs/themes on top if you want its default visual treatment out of the box. The headless primitive comes from `import { Button } from '@askrjs/ui/button'`, while `import { Button, ButtonGroup } from '@askrjs/themes/button'` (and `'@askrjs/themes/button-group'`) gives you the same component pre-wired to theme tokens. Because @askrjs/themes simply re-exports the @askrjs/ui Button rather than wrapping it, choosing one import path over the other only changes whether default CSS loads — behavior is identical either way.

Live examples

Sample the variant set (default, primary, secondary, outline, ghost, destructive, link) side by side to see how each maps to your theme's color tokens, then check the size scale from xs up through lg and the icon-only sizes. A disabled example and an asChild example — Button wrapping an <a> so a link is styled and behaves like a button — are worth clicking through directly, along with a ButtonGroup showing attached buttons in both horizontal and vertical orientation.

Anatomy

In its default mode Button renders exactly one native <button> element with your children inside it — no wrapper divs or extra nodes. With asChild set to true, it instead clones its single child element and merges its press behavior and props onto that child, which is how you turn an <a> or a custom component into a button without double-wrapping markup. ButtonGroup renders a single <div> around its Button children and applies data attributes for orientation and attachment; it doesn't manage focus or press behavior itself, that stays with each individual Button.

State model

Button has no internal state to track — variant, size, width, and disabled are just props you set on each render, and onPress is the only interaction callback (onClick is intentionally not part of the type). If you need a toggled or pressed-state button, that's a different component (Toggle), not something Button models itself. ButtonGroup is similarly stateless: attached and orientation are plain layout props, not values a group tracks or reports back.

Keyboard and accessibility

Space and Enter both trigger activation through the pressable foundation rather than through any key handlers wired up inside Button itself, keeping behavior aligned with how WAI-ARIA defines the button pattern. Native <button> elements inherit keyboard behavior and focusability from the browser for free, while asChild targets get the same tabindex and ARIA-disabled handling applied by the foundation to non-native elements. Disabling a button removes it from the tab order — the native disabled attribute handles this for real buttons, and aria-disabled plus tabindex="-1" cover the asChild case.

Styling and tokens

Themed buttons key off data-slot="button" plus data-variant, data-size, and data-width attributes that the CSS in @askrjs/themes reads directly — data-variant="primary" swaps in --ak-color-primary, data-size="sm" adjusts --_button-height and padding, and data-width="full" stretches the button to its container. Because these are just data-attribute selectors under :where(), overriding a variant's color or an icon button's size is a matter of targeting the same selector with your own CSS, no component prop needed. ButtonGroup's attached mode collapses the gap and shared border-radius between adjacent buttons; toggling orientation switches its data-orientation attribute between horizontal and vertical layout.

API

ButtonProps is a discriminated union: ButtonNativeProps (asChild omitted or false) accepts variant, size, width, disabled, onPress, and a type restricted to 'button' | 'submit' | 'reset', while ButtonAsChildProps requires asChild: true, a single child, and drops type and the native button attributes entirely. ButtonSize spans 'xs' | 'sm' | 'md' | 'lg' | 'icon' | 'icon-xs' | 'icon-sm' | 'icon-lg', and ButtonVariant covers 'default' | 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'link'. ButtonGroupProps adds attached?: boolean and orientation?: 'horizontal' | 'vertical' on top of ordinary div props.

Edge cases

Because Button defaults to type="button", a Button meant to submit a form has to be given type="submit" explicitly — this is deliberate, to stop buttons inside forms from submitting by accident. asChild requires exactly one child element; passing text or multiple children will not merge correctly since the props are applied directly to that single child. Long button labels wrap rather than overflow (the themed CSS sets white-space: normal and overflow-wrap: anywhere), so very long text grows the button vertically instead of clipping.

See Input and Textarea for the other native-element primitives that follow the same asChild pattern, Toggle Family if you actually need a pressed/unpressed state rather than a one-shot action, and Form, Label, Field, and Input Group for how Button composes inside a full form layout.