registry:ui · General
icon-button — icon and text, one contract
The button for actions that carry a glyph: an explicit two-part contract where icon is the glyph (always decorative) and text is the ONE label. The text posture renders icon + label side by side; iconOnly collapses to a square where the label does not disappear — it moves to the tooltip and stays the accessible name. The button itself IS a press-button (composition, not a copy): every paint variant, the component-tag attachment (r4 effect-attachments — the tag's symbol-keyed prop chains through this component's rest spread into press-button's own, landing at the wrapped button's root), href anchoring and the press law pass through verbatim — same physics, same shadow tokens, the same hit band (40px at the default rung, measured — the density-hit lane, not a literal; the ladder 28/32/40/48).
icon-button
The icon+text button: the text posture renders icon and label together; iconOnly renders the square and tips the label. The playground drives the lower instance.
<script lang="ts">
import IconButton from '@ui/icon-button.svelte';
import { pressEffect } from '@ui/press-button';
import { shimmer } from '@ui/press-button.svelte';
</script>
<!-- one label, two postures: text shows the label, iconOnly tips it -->
<IconButton iconOnly variant="ghost" placement="bottom" text="deploy">
{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><path d="m6 3 14 9-14 9Z"/></svg>{/snippet}
</IconButton>
<!-- iconOnly: text moves to the tooltip AND stays aria-label -->
<IconButton iconOnly text="copy command">
{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>{/snippet}
</IconButton>
<!-- full press-button inheritance: paint variants + effect attachments -->
<IconButton
iconOnly
variant="fill"
{@attach pressEffect(shimmer({ speed: 4000 }))}
text="open github"
href="https://github.com/jixoai/ui"
placement="bottom"
>
{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><path d="M7 17 17 7"/><path d="M7 7h10v10"/></svg>{/snippet}
</IconButton>demo
icon-only — the tooltip law
In icon-only the text is never thrown away: the same string feeds the tooltip (the hover/focus hint on the popover laws) and the accessible name (aria-label), so the button says itself to pointer users, keyboard users and screen readers from ONE source. The glyph is decorative by construction — the component wraps it aria-hidden.
icon-button · icon-only
<script lang="ts">
import IconButton from '@ui/icon-button.svelte';
</script>
<!-- hover / focus each one: the label tips AND names from ONE string -->
<IconButton iconOnly text="copy command" placement="bottom">
{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>{/snippet}
</IconButton>
<IconButton iconOnly text="open github" placement="bottom">
{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><path d="M7 17 17 7"/><path d="M7 7h10v10"/></svg>{/snippet}
</IconButton><script lang="ts">
import IconButton from '@ui/icon-button.svelte';
import { pressEffect } from '@ui/press-button';
import { shimmer } from '@ui/press-button.svelte';
</script>
<!-- one label, two postures: text shows the label, iconOnly tips it -->
<IconButton text="deploy">
{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><path d="m6 3 14 9-14 9Z"/></svg>{/snippet}
</IconButton>
<!-- iconOnly: text moves to the tooltip AND stays aria-label -->
<IconButton iconOnly text="copy command">
{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>{/snippet}
</IconButton>
<!-- full press-button inheritance: paint variants + effect attachments -->
<IconButton
iconOnly
variant="fill"
{@attach pressEffect(shimmer({ speed: 4000 }))}
text="open github"
href="https://github.com/jixoai/ui"
placement="bottom"
>
{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><path d="M7 17 17 7"/><path d="M7 7h10v10"/></svg>{/snippet}
</IconButton>law
One label, a full button
The contract is two props on purpose: icon carries the picture, text carries the meaning — and the button underneath is the real press-button, so nothing about a text button is missing here.
-
textis required and single-sourced: the text posture renders it beside the glyph, iconOnly turns it into the tooltip ANDaria-label— never handwritten twice -
iconis always decorative: the component wraps the snippetaria-hidden, so the glyph can never become the accessible name - inheritance is composition:
variant(paint),{@attach …}(the component tag — pressEffect(shimmer()) and kin, chained into the wrapped button's root),raised(physics — the foot-flat zone reaches the square through the same ambient read),hrefandclasspass straight through to press-button — one button, one law - the icon-only square rides the same hit band as a text button
(40px measured at the default rung) — rows stay level and the shadow
tokens are identical by construction;
placementpicks the tip's side (top default) and the tip's pointer notch is ON by default (arrowopt-out), aimed at the anchor point the placement names -
hrefswitches the element to an anchor; hrefs not starting with/open a new tab withnoreferrerautomatically
types
Text and icon-only postures
The component has two visual postures; both preserve the same press-button behavior and semantic label.
icon-button · postures
text: glyph and visible label
icon-only: tooltip and aria-label
<script lang="ts">
import IconButton from '@ui/icon-button.svelte';
</script>
<!-- the two postures; both keep the same press-button behavior -->
<IconButton text="deploy" variant="fill">{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><path d="m6 3 14 9-14 9Z"/></svg>{/snippet}</IconButton>
<IconButton iconOnly text="deploy" variant="fill">{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><path d="m6 3 14 9-14 9Z"/></svg>{/snippet}</IconButton>
<!-- flat physics: raised={false} is the engrave-tier inset press -->
<IconButton text="deploy" raised={false}>{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><path d="m6 3 14 9-14 9Z"/></svg>{/snippet}</IconButton>
<IconButton iconOnly tip={false} text="deploy" raised={false}>{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><path d="m6 3 14 9-14 9Z"/></svg>{/snippet}</IconButton>usage
Name the action once
Provide an icon snippet and a text label; icon-only mode promotes the same text into the tooltip and accessible name.
<script lang="ts">
import IconButton from '@ui/icon-button.svelte';
import { pressEffect } from '@ui/press-button';
import { shimmer } from '@ui/press-button.svelte';
</script>
<!-- one label, two postures: text shows the label, iconOnly tips it -->
<IconButton text="deploy">
{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><path d="m6 3 14 9-14 9Z"/></svg>{/snippet}
</IconButton>
<!-- iconOnly: text moves to the tooltip AND stays aria-label -->
<IconButton iconOnly text="copy command">
{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>{/snippet}
</IconButton>
<!-- full press-button inheritance: paint variants + effect attachments -->
<IconButton
iconOnly
variant="fill"
{@attach pressEffect(shimmer({ speed: 4000 }))}
text="open github"
href="https://github.com/jixoai/ui"
placement="bottom"
>
{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><path d="M7 17 17 7"/><path d="M7 7h10v10"/></svg>{/snippet}
</IconButton>a11y
Keyboard and label contract
The glyph is always decorative. The text prop remains the single source of meaning across both postures.
Keyboard
| Key | Action |
|---|---|
| Tab | Focus the native press-button control |
| Enter / Space | Activate the button |
| Escape | Close an icon-only tooltip |
ARIA
| Attribute | Value | Description |
|---|---|---|
| aria-label | text when iconOnly | Names an icon-only action from the text prop. |
| aria-hidden | true on icon | Prevents the decorative glyph from becoming the name. |
| role | button or link | Inherited from press-button according to href. |
theming
Inherited density
Icon-button forwards density to press-button, keeping its square and text postures aligned with the surrounding controls.
| Token | Default | Source |
|---|---|---|
| --jx-hit | 28 / 32 / 40 / 48px | density |
| --jx-text | 11 / 12 / 13 / 15px | density |
| --jx-line | 16 / 18 / 20 / 24px | density |
| --jx-gap | 8 / 8 / 12 / 16px | density |
| --jx-inset | 8 / 8 / 12 / 16px | density |
usage
Usage
Import the family parts and compose them in markup — the full usage file, as the canvas above runs it.
<script lang="ts">
import IconButton from '@ui/icon-button.svelte';
import { pressEffect } from '@ui/press-button';
import { shimmer } from '@ui/press-button.svelte';
</script>
<!-- one label, two postures: text shows the label, iconOnly tips it -->
<IconButton text="deploy">
{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><path d="m6 3 14 9-14 9Z"/></svg>{/snippet}
</IconButton>
<!-- iconOnly: text moves to the tooltip AND stays aria-label -->
<IconButton iconOnly text="copy command">
{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>{/snippet}
</IconButton>
<!-- full press-button inheritance: paint variants + effect attachments -->
<IconButton
iconOnly
variant="fill"
{@attach pressEffect(shimmer({ speed: 4000 }))}
text="open github"
href="https://github.com/jixoai/ui"
placement="bottom"
>
{#snippet icon()}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={cx(rt.ibSize4)}><path d="M7 17 17 7"/><path d="M7 7h10v10"/></svg>{/snippet}
</IconButton>axes
Universal props
The eight-axis surface (explicit-props): size · shape · radius · density · color · theme · elevation · motion — each axis takes named steps, auto (inherit the ambient context; stamps nothing), an exact number (px · coefficient · dp · hue per axis), or query() for responsive/container-conditional values. The restate resolves here and supplies the wrapped press-button through the §11 ambient broadcast — the shared control root stamps the carriers.
IconButton · universal props
<!-- the eight-axis surface rides the restate supply (§11) -->
<IconButton text="px number" size={14} density="small">{#snippet icon()}…{/snippet}</IconButton>
<IconButton text="named steps" size="large" radius="medium" iconOnly>{#snippet icon()}…{/snippet}</IconButton>api
Props
IconButton composes the press-button contract with an explicit label and decorative icon.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| icon * | Snippet | — | Decorative glyph content. |
| text * | string | — | Visible label, tooltip content, and accessible name. |
| variant | PressButtonVariant | 'ghost' · ambient zone | Press-button surface variant, ambient-manageable through the restated contract: omitted → the ambient paint zone (ButtonGroup / variant scope), else the frameless own (Owner 2026-09-05: the glyph IS the content, a frame around it is chrome noise). Explicit still wins. |
| raised | boolean | 'true' · ambient zone | The physics axis, forwarded verbatim: false is the FLAT texture (engrave-tier inset press, the body never moves). A card/dialog foot zone scopes the default to false through the same ambient read; an explicit prop always wins — the escape hatch for chrome that must stay convex. Inert on link. |
| iconOnly | boolean | false | Collapses into a square tooltip trigger. |
| placement | TooltipPlacement | 'top' | Icon-only tooltip placement. |
| arrow | boolean | true | Shows the tooltip pointer notch. |
| {@attach …} (component tag) | Attachment<HTMLElement> | — | The effect mount (r4): <IconButton {@attach pressEffect(shimmer())}> — the tag's symbol-keyed prop chains through this component's rest spread into press-button's own, landing at the wrapped button's stamped root. |
| …rest | HTMLAttributes<HTMLElement> | — | The rest lane: arbitrary attributes forward verbatim into the wrapped press-button — the same lane the component-tag attachment chains through. |
| href | string | — | Renders a link instead of a button. |
| external | boolean | auto | Controls external-link behavior. |
| onclick | () => void | — | Button activation handler. |
| type | 'button' | 'submit' | 'button' | Native button type. |
Universal props
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity across the motion kernels (§8) |