Pricing

Button handbook

v2.20.0 · @snowui-design-system/components

License: Before reading component documentation, source, or npm usage instructions for the first time, read the full LICENSE. Reuse that review for the same task and version. Public API configuration and internal implementation changes have different license implications. All components use the unified runtime package; source rights remain governed by the license.

Overview

Trigger actions or navigation with text or icons, including loading and disabled states, from primary actions to compact tool buttons.

When to use it

Use Button for actions. Use bare for an icon action without a shell. For a padded icon-only button, set iconOnly and provide aria-label. Use emphasis="muted" for a translucent bare button that becomes opaque on hover. Use Tab to switch content and Segmented for mutually exclusive choices.

  • The default is gray and sm when there is no context; explicit variant / size takes precedence over group configuration.

  • The pure icon must provide an accessible name; bare removes the shell, iconOnly keeps the button shell, and the component will not automatically judge according to the content.

Quick start

This example shows the recommended basic composition. See the sections below for more cases and composition limits.

<script lang="ts">	import { Button } from '@snowui-design-system/components';</script> <div style="display: flex; align-items: center; flex-wrap: wrap; gap: var(--gap-4) var(--gap-3);">	<Button variant="filled">Filled</Button>	<Button variant="gray">Gray</Button>	<Button variant="outline">Outline</Button>	<Button variant="borderless">Borderless</Button></div>

API

Keyword Type Default Description
variant "gray" | "filled" | "outline" | "borderless" — Button variant; explicit value takes priority, followed by the context embedded in ButtonGroup or Breadcrumb, and return gray when there is no context.
size "sm" | "md" | "lg" — Control button font size, inner margin, rounded corner and sub-icon size; explicit values take precedence, followed by ButtonGroup/set and parent size context, and sm when there is no context.
iconOnly boolean false The pure icon button mode with padding specializes in controlling the symmetrical padding and icon size when there are only icons; the sm/md/lg icon is 16/20/24px. Use bare directly when there is no shell icon button. Development environment warning when aria-label, aria-labelledby or title are missing.
bare boolean false There is no shell button, mainly icon operation, and can also contain text or other content. Ignore variant, remove padding, radius, background, border and hover overlay, and keep the content gap. There is no need to overlay iconOnly; the icon of the same size is the same as the iconOnly, which is 16/20/24px, and the explicit size of the sub-icon is preferred. The default is 100% opaque, and emphasis="muted" enables 40% → hover 100%.
emphasis "normal" | "muted" "normal" Visual emphasis for bare. normal is fully opaque at rest and on hover; muted changes from 40% to 100% opacity on hover. It does not affect other variants.
gap number — Gap token index. Use the existing size-based gap when omitted. Graphics and independent text content retain at least 4 CSS px; explicit zero uses that floor while larger preferred gaps are preserved. No outer Frame is required.
backgroundBlur boolean false Blur content behind the button with backdrop-filter: blur(1.25rem), matching BarChart’s blurred overlay.
scaleOnHover boolean — Whether to enable Button's built-in background layer hover magnification and supporting click retraction / restore motion at the same time. The independent action is turned on by default; the content follows the scaling by default, and the hit area and layout remain unchanged. The composite control container can provide the default value of false, and the sub-Button explicitly transmits true to reopen it. Bare has no background layer, so there is no visual effect; this Prop does not cover the explicit whileHover.
contentScaleOnHover boolean Follow the background to zoom Whether to let text, Icon, Spinner and other content follow the built-in background hover and click zoom as a whole. false only turns off content scaling; true still requires background scaling to take effect, and cannot bypass scaleOnHover=false, bare, disabled or explicit whileHover takeover.
toggle boolean false Toggle pattern grammar sugar. Internal automatic injection of Click={{ Idle: "Active", Active: "Idle" }} and whileClick=false; in the Active state, data-state="Active" automatically exposed by Frame triggers Brand color matching (gray / outline / borderless takes effect, filled excludes); aria-pressed is automatically output by Frame. The consumer can continue to transmit Click / whileClick explicit coverage.
holdRepeat boolean false Turn on the pointer and press and hold the serial delivery; it will only be effective for pressing the main button of the host button that is not disabled. Press to trigger a click immediately, wait for the cycle to trigger, release or cancel the stop; do not add a series to the link or keyboard.
holdRepeatDelay number 260 The waiting time before the serial delivery of the start cycle, the unit is ms, and the negative value is processed as 0; only holdRepeat=true is effective, and the first cycle click still needs to wait for holdRepeatInterval.
holdRepeatInterval number 80 The interval of adjacent cycle click during the press-down period, in ms, the minimum is 1; only holdRepeat=true is effective.
children Snippet — The text, icons or other content in the button; with the shell pure icon usage setting iconOnly, no shell use bare; the pure icon usage of both must provide accessible names, and the components are not automatically inferred through the content.

See the base component documentation for inherited capabilities not redeclared here. This table lists only named public Props declared by this component.

Usage boundaries and details

Component-Specific Props (Component-Specific Props)

Attribute Type Default Description
href string undefined Link address (rendered as a label)
target string undefined How to open the link
rel string undefined Link relationship

Keywords (Keyword)

Keyword Type Default Description
as 'more' | 'badge' | 'icon' | 'prev' | 'next' | 'close' | 'item' | 'current' | 'page' | 'leading' | 'trailing' - Roleization: As a Text expansion button, Breadcrumb item, Pagination template, or access to Image / ImageViewer / Field host positioning protocol; when entering the Image / ImageViewer host, it will be translated as the corresponding pin-* direction

Layout model

Main layout model: Inline shell layout. See the definition.. /.. /../specs/maintainers/LayoutProtocol.md §12.4 (source code reference: specs/maintainers/LayoutProtocol.md).

The internal layout of Button is not connected to the three-slot model:

  • The root node maintains the inline-level shell, and the content is Hug as a whole.

  • Icons, text, Spinners and other lightweight content use the shared single-line spacing resolver. Effective spacing stays at least 4 CSS px when root font size or gap-scale decreases; larger existing gaps are preserved. Icon-only content does not gain text or empty slots.

  • The default text type Button is white-space: nowrap, which ensures that the button copy remains a single line as an operation phrase; special long copy can be explicitly overwritten by the user through external style / class.

  • The default width and minimum width are max-content, that is, the Hug width of "content + gap + padding"; the default stretch of vertical Flex/Grid will not fill the Button. Only the exclusive combination layout of explicit width / Fill or component declaration can be widened, and the Button cannot be pressed to swallow the default padding.

  • Button itself is responsible for padding / gap / radius / font-size / line-height, and allows external overwriting through the --snowui-button-*-override variable.

  • Gray, filled, outline, and borderless variants use the shared .snowui-rounded-rectangle and --corner-shape-rounded-rectangle: superellipse(1.1). This curve is slightly smoother than round without changing the root corner area computed from --snowui-button-radius or --snowui-button-radius-override. The separate hover background inherits border-radius and corner-shape and scales with its background transform; the root Button’s radius and layout size stay fixed. iconOnly affects content layout only and does not imply circular corners. Browsers without corner-shape retain the existing border-radius. Bare and ButtonGroup-embedded buttons still use radius 0, with the same shell ownership.

  • iconOnly is the only switch of the single-visual content layout, and the default is false; Button does not read the DOM type of children to infer the state. The content can be Icon, Logo or other single visual atoms.

  • as="leading" / as="trailing" only represents Button as an accessory for the host of the outer slot such as Field / Input; it will not change the internal layout model of Button.

  • as="item" indicates Button as a path fragment of Breadcrumb; the non-interactive rule of the current page is triggered by Breadcrumb marking aria-current="page". When Button does not explicitly write variant, it can inherit the variant of Breadcrumb, and the explicit Button variant has priority.

  • as="current" / as="page" / as="prev" / as="next" indicates the role style template when Pagination is put; the real page number, clickability and boundary status are generated and managed by Pagination.

  • bare is a no-shell specialization that does not limit the content type: keep the current font size / line height and the ordinary content gap, fix zero padding / radius, and use currentColor × opacity: 1 by default; only emphasis="muted" uses 0.4 -> 1 to express secondary operations.

  • as="pageSize" can be used as a visual template for the number of pages per page of Pagination; Pagination will inject the current value and ArrowLineDown, and take over the interaction with the built-in Listbox. Icon, as a Button ordinary content, does not use as="trailing", and the spacing is controlled by the Button content gap.

  • bare automatically injects 1/1.25/1.5rem icon context consistent with iconOnly (16/20/24px under standard root font size), no need for iconOnly; sub-icon explicit size is preferred. The compatible bare iconOnly combination still retains the transparent hot area.

  • When Button is a direct sub-item of ButtonGroup, it automatically enters the embedded state: retains independent clicks, links, status and content gaps, withdraws its own background, border and rounded corners, and receives the cell padding assigned by ButtonGroup.

Layout variable chain:

Variable Default source Use
--snowui-button-padding-block Size gear The longitudinal inner margin of the shell
--snowui-button-padding-inline Size gear The horizontal inner margin of the shell
--snowui-button-padding padding-block + padding-inline Inner margin of the shell
--snowui-button-icon-only-padding Size gear The inner margin of the single icon button
--snowui-button-gap Size gear Icon / Text / Spinner Pitch
--snowui-button-radius Size gear Rounded corners of the shell
--snowui-button-font-size Size gear Text font size
--snowui-button-line-height Size gear Text line height

Usage examples

 <Button>Click me</Button>  <Button variant="filled">Submit</Button>  <Button href="/about">About</Button>  <Button bare>Random</Button><Button bare><Icon><Shuffle /></Icon>Random</Button><Button iconOnly aria-label="Expand"><ArrowLineDown /></Button><Button bare aria-label="Expand"><ArrowLineDown size={16} /></Button><Button bare emphasis="muted" aria-label="Secondary action"><ArrowLineDown size={16} /></Button><Button backgroundBlur>Blurred background button</Button>  <Field>  <Input type="date" />  <Button bare as="trailing" aria-label="Open date picker">    <ArrowLineDown size={16} />  </Button></Field>  <Button loading>Loading...</Button>

Update log (2026-09-12): Horizontal expansion increases the upper limit of rem, and the vertical ratio remains unchanged; Borderless starting point and click retraction synchronous axis compensation.

Hover size compensation

The built-in background motion is parsed by getButtonBackgroundScale(resolvedSize), and the timing and vertical default/sm baseline of BUTTON_HOVER_SCALE_UP_MOTION are retained. Take the standard height Hsm / Hsize as the compensation coefficient k: the hover end point is 1 + (sm end point - 1) × k; Borderless hidden starting point is 1 + (sm hidden starting point - 1) × k; click midpoint is 1 + (current hover end point - 1) / 2.

The current standard height of sm / md / lg is 24 / 36 / 48 (16px root nominal value), corresponding to the hover end point 1.06 / 1.04 / 1.03, and the Borderless starting point 0.84 / about 0.893333 / 0.92. The upper and lower edges of the standard height button are expanded by about 0.72px, and the complete unfolding stroke of Borderless is also consistent; the value is subject to the preset catalog (source code reference: components/src/motion/presets/README.md).

Adopt the size of the final analysis (explicit Prop takes precedence over Context). The total horizontal expansion is limited by preset to a maximum of 0.25rem (2px each on the left and right under the 16px root font size), and the original proportion is maintained vertically; Borderless hides the starting point and click retracts synchronously to use horizontal compensation. Only when the default background motion is enabled, the shared resizeManager is reused, and the geometry is refreshed at the beginning of initialization, size change and interaction, and no frame-by-frame measurement. When the root font size changes, the upper limit is scaled with rem; the rounded corner is scaled with the background sub-axis. Custom height / padding does not do vertical compensation again. The hit area, layout, color and timing remain unchanged. The content is scaled as a whole by default with the same sub-axis scale, and the original size can be retained through contentScaleOnHover={false}.

Hover supporting click effect

When the built-in hover magnification is enabled, each click (including keyboard activation) shrinks from the current display size of the background back to the midpoint of the current size standard hover interval, and then restores to the hover magnification size. All variants (including Borderless) share the same rules vertically, and the horizontal synchronization is compensated according to the above upper limit: sm is 1.06 → 1.03 → 1.06, md is 1.04 → 1.02 → 1.04, lg is 1.03 → 1.015 → 1.03; the specific value is subject to the preset catalog. The starting point of the Borderless hidden background is only used for hover, and does not participate in click retraction. The retraction and recovery sections each use half the duration of BUTTON_HOVER_SCALE_UP_MOTION to maintain the same easing; the current hover is 200ms, and the click is 100ms retraction + 100ms recovery. Text, Icon, Spinner as the overall default synchronization retract/restore; keep the original size when the content scaling is turned off. The hit area and layout maintain the original size, and click the callback, link and Toggle status to take effect immediately, without waiting for the motion.

scaleOnHover={false}, a composite control’s default off setting, bare, disabled, or explicit whileHover prevents this built-in motion. A Button nested in another component also follows its host’s setting. Disabling hover scale disables click scale; explicitly enabling hover scale for a composite child enables click scale too. Repeated clicks restart from the current background size without queuing, and release restores the actual hover state. Reduced-motion mode keeps the current hover or idle appearance without click scaling.

The content visual layer declares with data-tooltip-anchor-decoration that the outer Tooltip anchor point does not change, so as to avoid hover scaling interruption of continuous prompts. The content uses a stable visual host to carry text, icons and their spacing, and reuse root alignment and layout settings; switch changes do not rebuild children. When content scaling is enabled, the synthetic layer before and after the animation is maintained through will-change: transform to avoid the feeling of downward movement caused by text re-rasterization at the end. The background and content take the center of the button as the scaling origin, and use the same preset to avoid the independent magnification of each leaf. The Borderless content starts from the original size, and the background still retains the hidden starting point, and the two converge to the same hover ratio at the same time. Submit the final ratio directly when reducing the motion, without transition or click scaling.

When the built-in scaling is enabled and the reduction motion is not set, the basic background and hover layer retain their respective synthetic layers to avoid layer reconstruction and edge redrawing when repeatedly moving in/out; the cost is to retain the memory occupation of the background composite layer. The text continues to use the original transform synthetic animation, and does not change to frame-by-frame redrawing.

Effective clicks only read the current transform of the background and content, and click continuously to take over from the current screen; do not add Observer, timer or frame-by-frame JS tasks. SSR does not read the browser API.

<Button>Content scales with background by default</Button><Button contentScaleOnHover={false}>Scale background only</Button><Button scaleOnHover={false}>Scale neither background nor content</Button>

ButtonGroup embedded state

Button does not need to write additional props after putting it into ButtonGroup. ButtonGroup draws dividing lines between adjacent cells by default:

<ButtonGroup>  <Button as="item" onclick={copy}>    <Icon><Copy /></Icon>    <Text>Copy</Text>  </Button>  <Button as="item" onclick={openMore}>    <Icon><ArrowLineDown /></Icon>    <Text>More</Text>  </Button></ButtonGroup>
  • Button is still independent button / a, and events, href, state machine and accessibility attributes are all handled by Button itself.

  • Button's size / variant / disabled inherits ButtonGroup by default; the instance explicitly transmits size / variant can be overwritten.

  • The embedded state does not draw Button's own shell to avoid superimposing with the ButtonGroup shell.

  • The embedded state will receive the cell padding assigned by ButtonGroup, so that the hover/click response can overwrite the entire cell, not just overwrite the internal icons and text.

  • The beginning and end of the cell are automatically marked by ButtonGroup, and there is no need to write as="first" / as="last" on the Button.

  • Set showSeparator={false} on ButtonGroup when there is no need to divide the line; there is no need to insert Separator manually.

  • The embedded state still retains the existing capabilities of href / target / rel, state machine, motion and aria-* and other Buttons.

  • The pure icon cell must explicitly declare iconOnly on the Button; the ButtonGroup embedded state will overwrite icon-only padding to ensure that the click and hover areas of the icon cell are consistent with ordinary cells.

Toggle and instantaneous feedback (declarant)

Button does not provide independent Props such as pressed / feedback, nor does it use stateContent snippet to switch content. Both Toggle and instantaneous feedback modes are implemented through Frame's existing state machine + stateStyle** declarative - the content is fixed, and only inline-style is applied according to the state.

Toggle: Switch back and forth between the two states

Recommended writing method - toggle grammar sugar:

<Button toggle>    <Icon><Bell /></Icon>    Notifications</Button>

toggle=true Internal automatic injection:

  • Click={{ Idle: "Active", Active: "Idle" }}

  • whileClick=false

In the Active state, data-state="Active" automatically exposed by Frame triggers Brand color matching (background 10% + text / Icon --brand), and aria-pressed is automatically output by Frame. Consumers have zero samples.

Equality complete writing method - suitable for scenarios that require custom state machines:

<Button    whileClick={false}    Click={{ Idle: "Active", Active: "Idle" }}>    <Icon><Bell /></Icon>    Notifications</Button>

Active color matching is still automatically taken effect by data-state="Active", which is exactly the same as toggle.

Custom Active Vision:

If you need color matching other than Brand, the consumer can be overwritten by stateClass / stateStyle, and the default Brand selector of data-state can be overwritten by ordinary class / inline style in terms of style priority:

<Button    toggle    stateClass={{ Active: "bg-red-10 text-red" }}>    <Icon><Trash /></Icon>    Dangerous toggle</Button>

Activation State Color Matching Unified Rules (v1.5.0): Button and Tag share a set of activation state rules:

  • Path: Frame currentState === "Active" → DOM Injection data-state="Active" → Component CSS [data-state="Active"] Color Set

  • Color matching: background color-mix(var(--brand) 10%, transparent) + text var(--brand)

  • Scope: Button is effective for gray / outline / borderless, filled / link excludes to avoid conflicts; Tag single variants are all applicable

  • Consumer does not need to cite any internal class

Instant feedback: automatic bounce back after clicking

<Button    whileClick={false}    Click="Active"    Active={["Idle", "auto"]}    whileActive={{ duration: 2000 }}>    <Icon><Paperclip /></Icon>    Copy content</Button>
  • Click="Active" Click to enter Active

  • Active={["Idle", "auto"]} Automatically bounce back to Idle

  • whileActive={{ duration: 2000 }} Only used for Active duration (no dynamic attribute is declared)

  • Side effects (such as clipboard writing) hang on onclick; state switching and rebound are taken over by Frame.

  • During Active, the color matching is automatically set by data-state="Active" Brand, and there is no need to write stateClass by hand.

And aria-pressed / aria-selected(v1.5.0)

Button Automatic output aria-pressed:

  • When currentState === "Active", the root node outputs aria-pressed="true"; currentState === "Idle" outputs aria-pressed="false".

  • It is only enabled when the toggle dictionary of Idle ↔ Active appears in the state machine statement (such as Click={{ Idle:"Active", Active:"Idle" }}); ordinary Button without toggle semantics is not output.

  • When the consumer explicitly enters aria-pressed or aria-selected, external values take precedence, and the component is no longer overwritten (used for list selection contexts such as Button in listbox / tab).

The same rule is synchronously implemented in Tag. For details, please refer to Tag.md Barrier-free.

Combination with Field / Date Input

Button now only retains one layer of date trigger rules:

  • When Button as="trailing" and the only same layer Input type="date" are in the same recognizable local container, it will automatically proxy the date selector and hide the native date icon.

  • At present, the recognizable containers only include:

    • Field direct subnode

    • Frame as="row" This kind of .snowui-role-row container

  • The explicit declaration date trigger through additional behavior prop is no longer supported.

 <Field>  <Input type="date" />  <Button bare as="trailing" aria-label="Open date picker">    <ArrowLineDown size={16} />  </Button></Field>  <Field>  <Frame as="row" row>    <Input type="date" />    <Button bare as="trailing" aria-label="Open date picker">      <ArrowLineDown size={16} />    </Button>  </Frame></Field>

Combination with Breadcrumb

Use as="item" when Button is put into Breadcrumb:

<Breadcrumb>  <Button as="item" href="/dashboards">Dashboards</Button>  <Text as="separator">/</Text>  <Button as="item">Default</Button></Breadcrumb>
  • Breadcrumb will automatically mark the last as="item" as the current page.

  • The current page Button will receive aria-current="page" and data-current="page".

  • When Button meets data-as="item" and aria-current="page" at the same time, Button does not respond to hover, click, Enter or Space, and does not display hover overlay.

  • Button does not change size because of as="item"; when variant is not written, it can inherit the variant of Breadcrumb, and when Breadcrumb does not write variant, it continues to use the default value of Button.

Combination with Pagination

Use the role template when Button is put into Pagination:

<Pagination page={1} total={5}>  <Button as="current">1</Button>  <Button as="page">2</Button></Pagination>
  • The Button in Pagination is a role style template, not a page number data list.

  • as="current" defines the current page template; as="page" defines the non-current page template.

  • as="prev" / as="next" can define the template of the previous page / next page; Pagination uses the built-in default arrow when missing.

  • Pagination will replace the template text, generate real page numbers and take over the clicks.

The choice of Bare and IconOnly

  • bare: no shell button, mainly icon operation, suitable for lightweight tools and in-line actions; text is also allowed.

  • iconOnly: a special icon button with padding, which is used to control the symmetric padding and icon size when there are only icons; the shell style is determined by variant.

  • The icon size of the two is the same as the size; the text button with the icon still uses 12/16/20px. See the table below for the exact size.

  • The requirements of bare 12/16/20px and pure icon unified overlay iconOnly in the history have been replaced by user definition on 2026-09-11.

Button style overlay

Read the full LICENSE first; reuse that review for the same task, version, and scope.

First use public component Props to match the desired appearance, preserving the existing look where possible. If changes are necessary, follow the style selection and override order: use your own class (including Tailwind already in the project) or an instance style. The names below are public overrides actually consumed by source; check their scope and do not modify private DOM. Choose the correct SnowUI token family. External apps need not install SnowUI website utilities.

Variable Purpose and default reference
--snowui-button-background-blur-override Background blur intensity (1.25rem by default when backgroundBlur is turned on)
--snowui-button-background-override Background (see component source for the default).
--snowui-button-border-color-override Border color (see the component source code for the default value)
--snowui-button-border-width-override Border width (see the component source code for the default value)
--snowui-button-color-override Color (see the component source code for the default value)
--snowui-button-font-size-override font-size (see component source for the default).
--snowui-button-gap-override gap
--snowui-button-group-cell-gap-override Button cell internal gap
--snowui-button-group-cell-padding-override Unified overlay cell padding; the default is automatically assigned by the first/middle/tail position
--snowui-button-height-override Height (see the component source code for the default value)
--snowui-button-icon-only-padding-override Icon-only mode padding(sm=4px md=8px lg=12px)
--snowui-button-line-height-override line-height (see component source for the default).
--snowui-button-min-height-override Minimum height (see the component source code for the default value)
--snowui-button-min-width-override Minimum width (default max-content)
--snowui-button-opacity-override Opacity (see the component source code for the default value)
--snowui-button-padding-override padding y+x
--snowui-button-radius-override border-radius
--snowui-button-width-override Width (see the component source code for the default value)

Design Guide · Token · Combination boundary

Button type and status

Required Props: no named required Props.

Bindable Props: this component declares no named two-way binding; handle state updates through public callbacks or its host.

Inheritance statement: Omit<FrameProps, "size">. The inheritance ability is subject to the public document of the base class; the fields in Omit have been excluded and cannot be used as they are according to the base class.

Share Frame API · Return API

The following type declarations are actually referenced by public Props and come from source. A Svelte Snippet denotes a content fragment; ComponentProps denotes the Props of the corresponding component.