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.
API
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)
Keywords (Keyword)
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-*-overridevariable.Gray, filled, outline, and borderless variants use the shared
.snowui-rounded-rectangleand--corner-shape-rounded-rectangle: superellipse(1.1). This curve is slightly smoother thanroundwithout changing the root corner area computed from--snowui-button-radiusor--snowui-button-radius-override. The separate hover background inheritsborder-radiusandcorner-shapeand scales with its background transform; the root Button’s radius and layout size stay fixed.iconOnlyaffects content layout only and does not imply circular corners. Browsers withoutcorner-shaperetain the existingborder-radius. Bare and ButtonGroup-embedded buttons still use radius0, with the same shell ownership.iconOnlyis the only switch of the single-visual content layout, and the default isfalse; 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 markingaria-current="page". When Button does not explicitly writevariant, it can inherit thevariantof Breadcrumb, and the explicit Buttonvarianthas 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.bareis 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 usecurrentColor × opacity: 1by default; onlyemphasis="muted"uses0.4 -> 1to 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 andArrowLineDown, and take over the interaction with the built-in Listbox. Icon, as a Button ordinary content, does not useas="trailing", and the spacing is controlled by the Button content gap.bareautomatically 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:
Usage examples
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.
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:
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/variantcan 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 andaria-*and other Buttons.The pure icon cell must explicitly declare
iconOnlyon 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:
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:
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:
Activation State Color Matching Unified Rules (v1.5.0): Button and Tag share a set of activation state rules:
Path: Frame
currentState === "Active"→ DOM Injectiondata-state="Active"→ Component CSS[data-state="Active"]Color SetColor matching: background
color-mix(var(--brand) 10%, transparent)+ textvar(--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
Click="Active"Click to enter ActiveActive={["Idle", "auto"]}Automatically bounce back to IdlewhileActive={{ 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 outputsaria-pressed="true";currentState === "Idle"outputsaria-pressed="false".It is only enabled when the toggle dictionary of
Idle↔Activeappears in the state machine statement (such asClick={{ Idle:"Active", Active:"Idle" }}); ordinary Button without toggle semantics is not output.When the consumer explicitly enters
aria-pressedoraria-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 layerInput 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:
Fielddirect subnodeFrame as="row"This kind of.snowui-role-rowcontainer
The explicit declaration date trigger through additional behavior prop is no longer supported.
Combination with Breadcrumb
Use as="item" when Button is put into Breadcrumb:
Breadcrumb will automatically mark the last
as="item"as the current page.The current page Button will receive
aria-current="page"anddata-current="page".When Button meets
data-as="item"andaria-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
sizebecause ofas="item"; whenvariantis not written, it can inherit thevariantof Breadcrumb, and when Breadcrumb does not writevariant, it continues to use the default value of Button.
Combination with Pagination
Use the role template when Button is put into 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.
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.
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.