Navigation

v0.0.4
Forms

Chip

A small pill that is a filter or a removable value.

On this page

A chip is a small pill that is a choice or a value. Its ends are true half circles, which tells it apart from a badge: a badge describes, a chip can be acted on. As a filter, a chip is a <label> around a native checkbox or radio, raised at rest and filled with ink when chosen, the way a checkbox fills.

Chip

<fieldset class="flex flex-wrap gap-xs m(0) p(0) b(0)">  <legend class="mb-xs text-sm fg-text-strong">Topics</legend>  <label class="chip">    <input type="checkbox" class="chip-input" name="topic" value="design" checked>Design</label>  <label class="chip">    <input type="checkbox" class="chip-input" name="topic" value="engineering">Engineering</label>  <label class="chip">    <input type="checkbox" class="chip-input" name="topic" value="research" checked>Research</label>  <label class="chip">    <input type="checkbox" class="chip-input" name="topic" value="legal" disabled>Legal</label></fieldset>

#One of several

With radios that share a name, chips are a single choice, and the arrow keys move between them. Use this for a short set that changes a view at once; for a value in a form, a segmented control reads as one control.

One of several

<div class="flex flex-wrap gap-xs" role="radiogroup" aria-label="Period">  <label class="chip">    <input type="radio" class="chip-input" name="period" checked>Today</label>  <label class="chip">    <input type="radio" class="chip-input" name="period">7 days</label>  <label class="chip">    <input type="radio" class="chip-input" name="period">30 days</label>  <label class="chip">    <input type="radio" class="chip-input" name="period">All time</label></div>

#Values

Without an input a chip is a value: a recipient, a tag, an applied filter. It rests in a chip tone on one contour. chip-remove is a round button at its end; give it a name that includes the value. Put icon on a leading icon.

Values

<span class="chip">ada@example.com  <button type="button" class="chip-remove" aria-label="Remove ada@example.com"></button></span><span class="chip">alan@example.com  <button type="button" class="chip-remove" aria-label="Remove alan@example.com"></button></span><span class="chip">  <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">    <path d="M4 6h16M7 12h10M10 18h4" />  </svg>Status: Ready</span>

#Accessibility

  • The input inside a filter chip covers it invisibly, so the label is the target and the input keeps the state, the keys and the form value. Group filter chips in a <fieldset> with a <legend>, or a role="radiogroup" with a label.
  • The remove button's target reaches past its 20px glyph without moving it: 28px, and 36px on touch screens, where a filter chip grows to 44px like every other target.
  • A chosen chip is ink on moonstone and pearl on obsidian, with no hue, so it does not depend on color vision.

#Classes

ClassDescription
chipThe pill, 28px tall: a label around a chip-input, or a plain element that holds a value
chip-inputThe checkbox or radio inside a filter chip; it covers the chip invisibly, so the label is the target and the input keeps the state
chip-removeA round remove button at the end of a value; its accessible name includes the value

#Source

@master/ui/components/chip.css
/* * Chip: a small pill that is a choice or a value. * As a label around a checkbox or a radio it is a filter: raised at rest, filled with ink when chosen, * the way a checkbox fills. As a plain element it is a value that can carry a remove button. * A chip is 28px tall; its ends are true half circles, which tells it apart from a badge. */@layer components {  .chip {    --chip-height: 1.75rem;    --chip-padding-x: .625rem;    --icon-size: .875rem;    --icon-offset: -.0625rem;    position: relative;    display: inline-flex;    flex-shrink: 0;    align-items: center;    gap: .375rem;    min-height: var(--chip-height);    margin: 0;    padding: 0 var(--chip-padding-x);    border: 0;    border-radius: 999px;    background: var(--color-surface-chip);    color: var(--color-text-strong);    box-shadow: inset 0 0 0 .5px var(--color-line-contour);    font: var(--font-weight-control) .78125rem/1 var(--font-family-sans);    font-feature-settings: inherit;    white-space: nowrap;    vertical-align: middle;    -webkit-tap-highlight-color: transparent;    /* A chip that holds a .chip-input is a control: a small raised sheet that lifts under the pointer. */    &:has(.chip-input) {      --chip-lift: var(--lift);      @apply --surface-sheet;      box-shadow: var(--shadow-raised);      user-select: none;      -webkit-user-select: none;      cursor: pointer;      touch-action: manipulation;      transition-property: box-shadow, translate, color;      transition-duration: var(--duration-quick);      transition-timing-function: var(--easing-glide);      @media (hover: hover) {        &:hover:not(:has(.chip-input:disabled)) {          translate: 0 calc(-1 * var(--chip-lift));          box-shadow: var(--shadow-raised-hover);        }      }      &:active:not(:has(.chip-input:disabled)) {        translate: 0 var(--chip-lift);        transition-duration: var(--duration-press);      }    }    /* Chosen: ink, with the same line of light a checked checkbox has. */    &:has(.chip-input:checked) {      background: var(--color-surface-inverse);      color: var(--color-text-inverse);      box-shadow:        inset 0 .5px 0 var(--color-ink-highlight),        0 0 0 .5px var(--color-ink-edge),        0 1px 2px -1px var(--color-shade-1), 0 2px 4px -2px var(--color-shade-2);      @media (hover: hover) {        &:hover:not(:has(.chip-input:disabled)) {          box-shadow:            inset 0 .5px 0 var(--color-ink-highlight),            0 0 0 .5px var(--color-ink-edge),            0 1px 2px -1px var(--color-shade-1), 0 3px 6px -3px var(--color-shade-2), 0 6px 12px -6px var(--color-shade-3);        }      }    }    &:has(.chip-input:focus-visible) {      @apply --focus-ring;    }    &:has(.chip-input:disabled) {      background: var(--color-surface-inset);      color: var(--color-text-disabled);      box-shadow: var(--shadow-flat);      cursor: not-allowed;    }    /* A remove button stands as far from the pill's end as from its top and bottom, so the two circles are concentric. */    &:has(.chip-remove) {      padding-inline-end: calc((var(--chip-height) - 1.25rem) / 2);    }    /* On touch a filter is 44px, as every other target is. */    @media (pointer: coarse) {      &:has(.chip-input) {        min-height: 2.75rem;        padding-inline: 1rem;      }    }    @media (prefers-reduced-motion: reduce) {      &:has(.chip-input) {        --chip-lift: 0px;      }    }    @media (forced-colors: active) {      outline: 1px solid CanvasText;      outline-offset: -1px;      &:has(.chip-input:checked) {        background: Highlight;        color: HighlightText;        forced-color-adjust: none;      }    }  }  /* The checkbox or radio inside a chip covers it invisibly: the label is the target, the input keeps the state. */  .chip-input {    position: absolute;    inset: 0;    width: 100%;    height: 100%;    margin: 0;    padding: 0;    border: 0;    border-radius: inherit;    opacity: 0;    appearance: none;    cursor: inherit;  }  /* Removes the value; give it an accessible name that includes the value. */  .chip-remove {    position: relative;    display: inline-grid;    flex: none;    place-items: center;    width: 1.25rem;    height: 1.25rem;    margin: 0;    padding: 0;    border: 0;    border-radius: 999px;    background: transparent;    color: var(--color-text-muted);    appearance: none;    cursor: pointer;    touch-action: manipulation;    &::before {      content: '';      width: .75rem;      height: .75rem;      background: currentColor;      mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round'%3E%3Cpath d='M4.5 4.5l7 7M11.5 4.5l-7 7'/%3E%3C/svg%3E") center / contain no-repeat;    }    /* The target reaches past the glyph without moving it: 28px, and 36px on touch. */    &::after {      content: '';      position: absolute;      inset: -.25rem;      @media (pointer: coarse) {        inset: -.5rem;      }    }    @media (hover: hover) {      &:hover {        background: var(--color-line-contour);        color: var(--color-text-strong);      }    }    /* Pressed: a firmer tone than the pointer's, so a tap is answered where there is no pointer. */    &:active {      background: var(--color-line-strong);      color: var(--color-text-strong);    }    &:focus-visible {      @apply --focus-ring;      outline-offset: 0;    }    @media (forced-colors: active) {      &::before {        background: ButtonText;      }    }  }}