jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

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).

text · icon-only inherits press-button verbatim variants + {@attach} chain through aria-label never handwritten twice motion-reduce safe

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.

driven by the playground
icon-button-usage.svelte svelte
<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

hover / focus each one
icon-button-icon-only-demo.svelte svelte
<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>
usage
<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.

  • text is required and single-sourced: the text posture renders it beside the glyph, iconOnly turns it into the tooltip AND aria-label — never handwritten twice
  • icon is always decorative: the component wraps the snippet aria-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), href and class pass 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; placement picks the tip's side (top default) and the tip's pointer notch is ON by default (arrow opt-out), aimed at the anchor point the placement names
  • href switches the element to an anchor; hrefs not starting with / open a new tab with noreferrer automatically

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

icon-button-postures-demo.svelte svelte
<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.

usage
<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

KeyAction
TabFocus the native press-button control
Enter / SpaceActivate the button
EscapeClose an icon-only tooltip

ARIA

AttributeValueDescription
aria-labeltext when iconOnlyNames an icon-only action from the text prop.
aria-hiddentrue on iconPrevents the decorative glyph from becoming the name.
rolebutton or linkInherited 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.

xs
sm
default
lg
TokenDefaultSource
--jx-hit28 / 32 / 40 / 48pxdensity
--jx-text11 / 12 / 13 / 15pxdensity
--jx-line16 / 18 / 20 / 24pxdensity
--jx-gap8 / 8 / 12 / 16pxdensity
--jx-inset8 / 8 / 12 / 16pxdensity

usage

Usage

Import the family parts and compose them in markup — the full usage file, as the canvas above runs it.

IconButton usage
<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

icon-button-universal.svelte svelte
<!-- 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

PropertyTypeDefaultDescription
icon * Snippet—Decorative glyph content.
text * string—Visible label, tooltip content, and accessible name.
variant PressButtonVariant'ghost' · ambient zonePress-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 zoneThe 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 booleanfalseCollapses into a square tooltip trigger.
placement TooltipPlacement'top'Icon-only tooltip placement.
arrow booleantrueShows 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 booleanautoControls external-link behavior.
onclick () => void—Button activation handler.
type 'button' | 'submit''button'Native button type.

Universal props

PropertyTypeDefaultDescription
size'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopethe base scale — root font-size; parts size in em
shape'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto' · ambient scopecorner geometry (CSS corner-shape; §14 degrade table)
radius'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopecorner size; auto = the concentric broadcast (§3)
density'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopespacing/leading scale over the kernel channels (§4)
color'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto' · ambient scopethe hue axis of the fixed oklch primary system (§5)
theme'light' | 'dark' | 'system' | 'auto''auto' · ambient scopelight/dark profile; system = the JS-mutable global (§6)
elevation'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto' · ambient scopeofficial M3 levels over the surface ladder (§7)
motion'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto' · ambient scopeintensity across the motion kernels (§8)
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h