registry:ui · General
chip — the grammar's compact activation
What a filter, a toggle, or an inline nav target looks like in this language: the badge's uppercase micro-label at badge scale with an activation root — button or anchor, press physics, the focus law. Paint is the frozen four-rung ladder (fill / tonal / outline / ghost) consumed as global tokens; hue is injected through classes, never named.
$npx jixoai-ui add chip
https://ui.jixoai.com/r/chip.json
overview
Overview
The badge's activation twin: one label, one ladder, two slot lanes, and a root that is a real button or a real anchor.
Chip is the grammar's compact activation — a control that filters, toggles, or
navigates without leaving the label's size class. The scale law is an Owner ruling
(2026-09-01, superseding the control-scale hit-lane floor): chip geometry is badge
geometry verbatim — height from the secondary line, inline insets only, never block
padding. The ONLY structural difference from a Badge is the activation root: a real <button>, or a real <a> when href is set, with the shared press physics and the focus law.
The paint is the variant-grammar ladder consumed as global tokens — fill for the one active filter, tonal for the resting set
(the frozen own default), outline for structure, ghost for
the quiet seats. Semantic hue is INJECTED into the four grammar slots through the jx-hue-* classes, never named as a variant. Effect loops are opt-in
through the component tag: {@attach pressEffect(ripple())} mounts
press-point ink from the shared press-button runtime onto the stamped root — a bare
chip is plain.
Two snippet lanes (slotStart / slotEnd) carry leading and
trailing glyphs at the label scale under the data-icon law. The universal axes
resolve on the same root — three are consumed here (size, density, theme), four are
supply-only, and the shape axis is deliberately absent: the family's own shape prop ('square' | 'pill') owns the name. The axis
grammar (named steps, auto, numbers, query()) is documented once on the universal props page.
usage
Usage
Import the component, pick the ladder step, inject hue through the classes when the intent is semantic, and arm effects through the component tag.
<script lang="ts">
import Chip from '@ui/chip/chip.svelte';
import { pressEffect } from '@ui/press-button';
import { shimmer, ripple } from '@ui/press-button/press-button.svelte';
</script>
<!-- the grammar ladder: prominence, never semantic hue -->
<Chip variant="tonal">filter</Chip>
<Chip variant="fill">deploy</Chip>
<Chip>filters</Chip><!-- tonal default; effect-free plain host -->
<Chip variant="outline">cancel</Chip>
<Chip variant="ghost">dismiss</Chip>
<!-- hue is injected into the global slots, never named as a variant -->
<Chip variant="tonal" class="jx-hue-success">passing</Chip>
<Chip variant="fill" class="jx-pair-destructive">clear</Chip>
<!-- one opt-in effect loop per chip — the component tag (r4: the
effect prop, its default ripple and the interim record retired;
pressEffect() self-listens, ink on pointerdown/Enter/Space) -->
<Chip variant="fill" {@attach pressEffect(shimmer())}>upgrade</Chip>
<Chip {@attach pressEffect(ripple({ shape: 'bevel', duration: 800 }))}>filter</Chip>
<Chip>still — a plain chip carries no loop by default</Chip>
<!-- href renders an anchor; hrefs outside "/" open a new tab -->
<Chip variant="outline" href="/docs.html">read the docs</Chip>
<!-- slotStart / slotEnd lanes keep svg at the label scale -->
<Chip shape="pill">
{#snippet slotStart()}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 5h18l-7 8v5l-4 2v-7L3 5z" /></svg>
{/snippet}
filters
</Chip>chip
The grammar ladder at badge scale. The top row is the four variants; the second row shows the plain effect-free chip, the bevel-silhouette ripple through the component tag, and the shimmer loop; the bottom instance is driven by the playground.
<script lang="ts">
import Chip from '@ui/chip/chip.svelte';
import { pressEffect } from '@ui/press-button';
import { shimmer, ripple } from '@ui/press-button/press-button.svelte';
</script>
<!-- the grammar ladder: prominence, never semantic hue -->
<Chip variant="tonal" shape="square">filter</Chip>
<Chip variant="fill">deploy</Chip>
<Chip>filters</Chip><!-- tonal default; effect-free plain host -->
<Chip variant="outline">cancel</Chip>
<Chip variant="ghost">dismiss</Chip>
<!-- hue is injected into the global slots, never named as a variant -->
<Chip variant="tonal" class="jx-hue-success">passing</Chip>
<Chip variant="fill" class="jx-pair-destructive">clear</Chip>
<!-- one opt-in effect loop per chip — the component tag (r4: the
effect prop, its default ripple and the interim record retired;
pressEffect() self-listens, ink on pointerdown/Enter/Space) -->
<Chip variant="fill" {@attach pressEffect(shimmer())}>upgrade</Chip>
<Chip {@attach pressEffect(ripple({ shape: 'bevel', duration: 800 }))}>filter</Chip>
<Chip>still — a plain chip carries no loop by default</Chip>
<!-- href renders an anchor; hrefs outside "/" open a new tab -->
<Chip variant="outline" href="/docs.html">read the docs</Chip>
<!-- slotStart / slotEnd lanes keep svg at the label scale -->
<Chip shape="pill">
{#snippet slotStart()}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 5h18l-7 8v5l-4 2v-7L3 5z" /></svg>
{/snippet}
filters
</Chip>demo
Button or anchor
href switches the root from button to anchor — internal hrefs navigate in place, anything else opens a new tab with noreferrer automatically. A chip without href is a real button: the onclick demo toggles its own label.
chip · anchors
<script lang="ts">
import Chip from '@ui/chip/chip.svelte';
let following = $state(false);
</script>
<!-- internal hrefs navigate in place; anything else opens a new tab -->
<Chip variant="outline" href="/docs/components.html">overview</Chip>
<Chip variant="outline" href="https://github.com/jixoai/ui">
{#snippet slotEnd()}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M7 17 17 7" />
<path d="M7 7h10v10" />
</svg>
{/snippet}
<span>github</span>
</Chip>
<!-- no href: a real button -->
<Chip variant="tonal" onclick={() => (following = !following)}>
{following ? 'following' : 'follow'}
</Chip>demo
slotStart and slotEnd lanes
Two optional snippet lanes wrap their content in data-icon spans and pin composed svg to the label scale (var(--jx-text-secondary)) — icons never outgrow the micro-label voice, and spacing comes from the root's half-gap.
chip · slots
<script lang="ts">
import Chip from '@ui/chip/chip.svelte';
</script>
<!-- slotStart: the leading icon lane -->
<Chip shape="pill">
{#snippet slotStart()}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M3 5h18l-7 8v5l-4 2v-7L3 5z" />
</svg>
{/snippet}
filters
</Chip>
<!-- both lanes; svg pinned to the label scale -->
<Chip variant="outline" shape="pill">
{#snippet slotStart()}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="m12 2 2.4 7.6H22l-6 4.4 2.2 7-6.2-4.6L5.8 21 8 14 2 9.6h7.6L12 2z" />
</svg>
{/snippet}
starred
{#snippet slotEnd()}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="m9 18 6-6-6-6" />
</svg>
{/snippet}
</Chip>law
The badge's activation twin
Chips ride badge geometry verbatim (Owner ruling, 2026-09-01, superseding the control-scale hit-lane floor): height from the secondary line, inline insets only, never block padding — the ONLY structural difference from a Badge is the activation root.
chip · badge twin
<script lang="ts">
import Chip from '@ui/chip/chip.svelte';
import Badge from '@ui/badge/badge.svelte';
</script>
<!-- badge geometry verbatim; the ONLY difference is the activation root -->
<Chip variant="tonal">activation</Chip>
<Badge>display</Badge>- the scale law: badge geometry verbatim — height from
--jx-line-secondary, inline insets from--jx-inset, never block padding; what makes it a chip is the activation root, not a bigger box. The hit lane is deliberately NOT a chip channel — the pseudo-element lane expansion stays rejected; the real box is the target - slotStart/slotEnd lanes replace their side's padding (the data-icon law): the icon lane carries the edge at half the inset — the same law badge practices and the input's edge glyph lanes (clear, steppers) take to zero
- the silhouette vocabulary is the family's own
shapeprop:squarekeeps the site radius (var(--jx-radius)— not sharp corners),pillrounds fully (calc(infinity * 1px))
theming
Hue injection and tokens
Color is injected, never named: the jx-hue-* / jx-pair-* intent utilities re-point the four grammar slots above a chip and every slot consumer inside retunes. Geometry rides the density channels.
chip · hue injection
<script lang="ts">
import Chip from '@ui/chip/chip.svelte';
</script>
<!-- hue is injected into the global slots, never named as a variant -->
<Chip class="jx-hue-success">passing</Chip>
<Chip class="jx-hue-warning">degraded</Chip>
<Chip class="jx-hue-neutral">metadata</Chip>
<Chip variant="fill" class="jx-pair-destructive">clear</Chip>| Token | Default | Source |
|---|---|---|
| --jx-inset | 8 / 12 / 16px at sm / default / lg | density |
| --jx-gap | density scale | density |
| --jx-text-secondary | 11 / 12 / 14px at sm / default / lg | density |
| --jx-line-secondary | density scale | density |
| --jx-radius | the site radius | structural |
| --jx-fill | var(--primary) | color |
| --jx-fill-ink | var(--primary-foreground) | color |
| --jx-tonal | var(--primary) | color |
| --jx-outline | var(--border) | color |
api
API
The table renders from the GENERATED meta; the seven axis props split into the shared Universal props section beneath the family rows. shape stays in the family table — it is the silhouette prop, not the axis (it rides the curation's extra lane because the shared split filters its name).
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| variant | 'fill' | 'tonal' | 'outline' | 'ghost' | 'tonal' · ambient zone | The grammar ladder — prominence, never semantic hue. fill is the solid ground with the same-hue border; tonal the 12%/45% color-mix tint recipe; outline the structural border with an 8% hover overlay; ghost the transparent frame that presses without a shadow and hovers from --jx-tonal. Availability is the frozen variant-grammar table (all four rungs; link never reaches chip). Omitted → the ambient paint zone, else the frozen own tonal. |
| href | string | — | Renders an anchor instead of the button root — the ONLY structural difference from a Badge. Internal hrefs (starting with '/') navigate in place. |
| external | boolean | — | Opens non-internal hrefs in a new tab. Defaults to auto: any href not starting with '/' gets target="_blank" + rel="noreferrer". |
| onclick | () => void | — | Activation handler — fires on button press and on anchor click alike. |
| type | 'button' | 'submit' | 'button' | Native button type for the button root. |
| ariaLabel | string | — | Accessible name override — reach for it on icon-only chips. |
| class | string | '' | Appended to the composed classes. Hue injection rides here: the jx-hue-* / jx-pair-* intent utilities re-point the four grammar slots (--jx-fill, --jx-fill-ink, --jx-tonal, --jx-outline). Same-property overrides need the consumer ! — utility order is not guaranteed. |
| slotStart | Snippet | — | Leading lane, wrapped in a data-icon span. The data-icon law replaces the side padding (inset halves) and pins composed svg to var(--jx-text-secondary) — the label scale. |
| slotEnd | Snippet | — | Trailing lane, same law as slotStart: the side padding halves and svg rides the label scale. |
| children * | Snippet | — | The chip label — the uppercase micro-label voice at badge scale. |
| shape | 'square' | 'pill' | 'square' · Own default, not ambient | The family's silhouette vocabulary — square keeps the site radius var(--jx-radius) (not sharp corners), pill rounds fully (calc(infinity * 1px)). NOT the universal §2 shape axis: the name collision left the axis out rather than renamed (migration-census.md batch B; flagged for the W6 Owner dossier — a future rename ruling could adopt the axis). Rendered from extra: the shared split filters this name from the generated rows. |
| {@attach …} (component tag) | Attachment<HTMLElement> | — | The effect mount (r4): <Chip {@attach pressEffect(ripple())}> compiles to a symbol-keyed prop that rides the rest spread onto the activation root, where the shared press-button runtime mounts the ink. A bare chip is plain — zero effect knowledge. Reduced motion freezes every loop. |
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) |
axes
The eight axes on this component
What each axis drives HERE — the carrier names are the family's real vars (chip.stylex.ts, chip.css, stampCarriersForLanes), steps and units per universal-props.schema.ts, every axis defaulting auto. Three are consumed on the chip itself (size on the glyphs, density's named rungs on the whole box, theme's dark bridge); four are supply-only (radius, color, elevation, motion — the consuming blocks are keyed to [data-jx-press-button] and the theme sheet's slot re-declarations, none of which the chip root carries); the shape axis is ABSENT — the family-local silhouette prop owns the name (migration-census.md batch B; W6-dossier-flagged).
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | CONSUMED — on the GLYPHS only. The kernel stamps --jx-size-effective and font-size inline, and the inline stamp outranks the base atom's density-anchored label size (14 → 14px; large → var(--jx-size-large) = 18px). The BOX does not follow: height, insets and the slot svg stay on the density lane (the badge-twin law — measured: a 24px label in the 20px default box). Number unit: px. |
| density | 'small' | 'medium' | 'large' | 'xs' | '2xs' | 'sm' | 'default' | 'lg' | 'auto' | number | 'auto' | CONSUMED — the named rungs. The rung stamps data-density and the kernel's scope block re-declares the channels the twin reads ON the root (--jx-inset, --jx-gap, --jx-text-secondary, --jx-line-secondary): inset 8 / 12 / 16px, label 11 / 12 / 14px, height ≈18 / 20 / 23px at sm / default / lg — the whole badge-twin geometry re-bases. small / medium / large alias sm / default / lg; the five legacy spellings stay addressable. A coefficient number stamps --jx-density-coefficient only — no rung attribute matches, nothing recomposes on the chip (the declaring-element law; measured: box unmoved at 1.5). Number unit: coefficient. |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' | CONSUMED — one-way. dark stamps the .dark class bridge on the root, and the theme sheet's .dark block re-declares the four grammar slots (--jx-fill, --jx-fill-ink, --jx-tonal, --jx-outline) there — the variant paint re-resolves the dark profile. light and system stamp nothing: they ride tree inheritance, so a light chip inside a dark tree stays dark. No number lane. |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-radius-effective; no family css reads it (grep receipt: zero --jx-radius-effective readers in the chip atoms — square paints the site radius var(--jx-radius), pill paints calc(infinity * 1px)). The consumers are the concentric downstream (press-button's corner calc, card, tooltip, menubar read the same carrier). Measured: a stamped 10px carrier under an 8px computed corner. Documented absence on the chip itself. Number unit: px. |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' | ABSENT — the family-local shape prop ('square' | 'pill', the silhouette corner-law vocabulary) owns the name and 'pill' is outside ShapeLane; not a §13-ruled rename family, so the axis is left out (forwarding ambient) rather than renamed — flagged for the W6 Owner dossier as an open refinement (migration-census.md batch B, LANDED). See the family shape prop in the API table. |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' | SUPPLY-ONLY — stamps --jx-color-effective; no family css reads it (grep receipt: zero --jx-color-effective readers in the chip atoms). The variant atoms consume the four grammar slots, and the slot re-derivation from the carrier is keyed to [data-jx-press-button] — the chip root never carries it (measured: an error carrier leaves the primary tint). Named → the §12 var indirection; a number → hue degrees through the oklch formula; raw strings pass through, closed at build. Hue that paints rides the jx-hue-* / jx-pair-* classes. Number unit: hue degrees. |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-elevation-effective; no family css reads it (the only consumer of elevation on the press side is --jx-elevation-shadow, stamped by the press-button component's own pair — the chip never stamps it). The chip's one shadow is the shared .jx-press rest (var(--shadow-xs)), and ghost nulls it. Measured: the rest shadow unchanged under a stamped level. Number unit: dp. |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-motion-effective; no family css reads it. The press law's transition chain is 150ms literals with a prefers-reduced-motion kill, and the effect loops freeze the same way — the axis has no chip-local kernel to step. Number unit: coefficient. |
The consumed lanes, live
size rides the glyph lane while the box stays density-anchored; the sm rung re-bases the whole twin; the coefficient number recomposes nothing; the dark bridge re-resolves the paint; one real query() case — 14px below the 48rem viewport, 16px at md and wider (resize the window); and the supply-only pair — the color carrier changes nothing while the jx-hue-error class next to it repaints.
size number · glyphs only — the box stays 20px
named step · var(--jx-size-large)
sm rung · data-density re-bases the channels
coefficient · no rung attribute, box unmoved
.dark bridge · the four grammar slots re-resolve
query() · 14px base, 16px at md and wider
supply-only · --jx-color-effective is unread
the lane that paints · --jx-tonal injected
<script lang="ts">
import Chip from '@ui/chip/chip.svelte';
import { query } from '$lib/universal-props-query.svelte';
</script>
<!-- CONSUMED — size rides the glyph lane; the box stays density-anchored -->
<Chip size={14}>14px glyphs, the default 20px box</Chip>
<Chip size="large">the named step resolves var(--jx-size-large)</Chip>
<!-- CONSUMED — the named rungs re-base the channels the twin reads -->
<Chip density="small">the sm rung: inset 8px, label 11px, height 18px</Chip>
<Chip density={1.5}>a coefficient stamps no rung — nothing recomposes</Chip>
<!-- CONSUMED — dark stamps the .dark class bridge on the root -->
<Chip variant="fill" theme="dark">dark profile</Chip>
<!-- one real query() case: 14px below the 48rem viewport, 16px at md -->
<Chip size={query({ md: 16 }, 14)}>responsive label</Chip>
<!-- SUPPLY-ONLY vs the lane that paints -->
<Chip color="error">the carrier stamps — the paint stays primary</Chip>
<Chip class="jx-hue-error">jx-hue-error injects --jx-tonal — the paint moves</Chip>a11y
Accessibility
Native buttons and anchors retain their platform behavior; the hit target is the real box at badge scale; motion degrades under both reduced-motion and forced-colors.
Keyboard
| Key | Action |
|---|---|
| Tab | Move focus to the chip or link |
| Enter / Space | Activate a button chip |
| Enter | Follow an href rendered as an anchor |
ARIA
| Attribute | Value | Description |
|---|---|---|
| aria-label | optional | Names icon-only or otherwise unlabeled chips. |
| data-jx-chip | variant | Hook attribute carrying the ladder step for styling and tooling. |
| prefers-reduced-motion | supported | Kills the press transition and freezes every effect loop; the anchored press still answers. |
| forced-colors | supported | Explicit system-color degradation; the 2px Highlight focus ring survives. |
The hit-floor note: the chip deliberately rides badge geometry BELOW the control-scale hit lane — roughly 20px tall at default density — by the Owner's 2026-09-01 scale ruling (the badge twins are activation, not targets that float). The activation target is still the whole real box: pseudo-element lane expansion stays rejected, so what you see is what you press. Under forced colors the color-mix tints drop explicitly (design §6): fill becomes ButtonFace/ButtonText, tonal and outline become Canvas/CanvasText, ghost rests transparent and takes ButtonFace on hover — and the 2px Highlight focus ring is never removed.