component
Chip
A small outlined label you can click, for filters and quick choices.
Hover, press or tab to one to see the other 3 states.
import { Chip } from 'rost/components/chips/Chip'
<div role="group" aria-label="Filter by category">
<Chip aria-pressed="false">Product</Chip>
<Chip aria-pressed="false">Brand</Chip>
</div>src/components/chips/Chip.tsx
import type { ButtonHTMLAttributes, ReactNode } from 'react'
import './chips.css'
export interface ChipProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** One or two words. */
readonly children?: ReactNode
/** `span` draws it without the button, for a preview inside something that's already a link or a button. */
readonly as?: 'button' | 'span'
}
const b = 'rost-chips'
/**
* A filter or a quick pick. Always a button: hover, press and focus are the
* browser's. There's no selected chip in the kit yet, so there's no
* `selected` prop either. Built from contracts/chips.contract.yaml.
*/
export function Chip({ children, className, type = 'button', as = 'button', disabled, ...rest }: ChipProps) {
const Root = as
const own = as === 'button' ? { type, disabled } : { 'aria-disabled': disabled || undefined }
return (
<Root className={[b, `${b}--status-default`, className].filter(Boolean).join(' ')} {...own} {...(rest as object)}>
<span className={`${b}__label`}>{children}</span>
<span className={`${b}__outline`} />
<span className={`${b}__focus-ring`} />
</Root>
)
}When to use
- Used for filters and quick choices in a row: categories, date ranges.
When not to use
- A status nobody clicks: Badge.
- A topic on a piece of content: Tag.
Behaviour
One status (Default) and five states. Hover, pressed and focus come from the browser. There's no selected chip in the kit yet, so a selected chip needs designing before anyone builds one. Not an improvised fill.
Accessibility
- Element:
<button type="button">, witharia-pressedwhen it filters. - Group: wrap the set in something with a name (
role="group"andaria-label="Filter by category"). - Focus:
focus-ring, on:focus-visible.
Content
One or two words, sentence case.
Props
| Prop | Type | Values | Default |
|---|---|---|---|
| status | enum | Default | Default |
States: Default, Hover, Pressed, Focus, Disabled.
Where it's used
- runceanu.com
src/rost/chips/Chip.tsx.rost-chips
Where code differs on purpose
- runceanu.com:Drawn as a `<span>` (`as="span"`) in the case-study meta, with no hover, press or cursor.Nobody clicks those. The guidance says a static topic is a Tag, but the site's design has the outlined chip there, so I kept the look and took the states off.
Values
Straight from the contract. Each value sits next to its token, because six tokens here all come out as #3a3a3a and the colour alone can't tell you which one is right.
Never changes
| Part | Property | Value | Token |
|---|---|---|---|
| root | justify-content | center | layout |
| root | align-items | center | layout |
| root | display | inline-flex | layout |
| root | vertical-align | middle | layout |
| root | position | relative | layout |
| root | background | none | Figma frame size |
| root | border | none | Figma frame size |
| root | font | inherit | Figma frame size |
| root | cursor | pointer | Figma frame size |
| root | outline | none | Figma frame size |
| root | gap | 8px | spacing/gap/small |
| root | height | 24px | spacing/size/small |
| root | padding-bottom | 2px | spacing/gap/3xsmall |
| root | padding-top | 0px | spacing/gap/none |
| root | padding-left | 8px | spacing/gap/small |
| root | padding-right | 8px | spacing/gap/small |
| root | border-radius | 10000px | spacing/radius/full |
| label | flex-shrink | 0 | layout |
| label | white-space | nowrap | layout |
| label | text-align | center | layout |
| label | position | relative | layout |
| label | font-family | 'General Sans Variable' | font-family/base |
| label | font-weight | 500 | typography/label/font-weight/medium |
| label | line-height | 14px | typography/label/line-height |
| label | font-size | 12px | typography/label/font-size |
| label | letter-spacing | 0px | typography/label/letter-spacing |
| outline | border-sides | all | layout |
| outline | position | absolute | layout |
| outline | inset | 0 | layout |
| outline | border-radius | 10000px | spacing/radius/full |
| outline | pointer-events | none | Figma frame size |
| focus-ring | display | none | layout |
| focus-ring | position | absolute | layout |
| focus-ring | inset | calc(-1 * var(--spacing-gap-2xsmall)) | layout |
| focus-ring | border-style | solid | no token yet |
| focus-ring | border-width | 2px | spacing/outline/medium |
| focus-ring | border-color | #999 | actions/secondary/regular/focus/focus-ring |
| focus-ring | border-radius | 10000px | spacing/radius/full |
| focus-ring | pointer-events | none | Figma frame size |
Changes by variant
focus-ring.display1
| status=Default, state=Focus | block | layout |
label.color5
| status=Default, state=Default | #d3d9d5 | actions/secondary/regular/default/label |
| status=Default, state=Hover | #ededed | actions/secondary/regular/hover/label |
| status=Default, state=Pressed | #d3d9d5 | actions/secondary/regular/pressed/label |
| status=Default, state=Disabled | #d3d9d5 | actions/secondary/regular/disabled/label |
| status=Default, state=Focus | #d3d9d5 | actions/secondary/regular/focus/label |
outline.border-color5
| status=Default, state=Default | #ededed | actions/secondary/regular/default/outline |
| status=Default, state=Hover | #ededed | actions/secondary/regular/hover/outline |
| status=Default, state=Pressed | #ededed | actions/secondary/regular/pressed/outline |
| status=Default, state=Disabled | #ededed | actions/secondary/regular/disabled/outline |
| status=Default, state=Focus | #ededed | actions/secondary/regular/focus/outline |
outline.opacity5
| status=Default, state=Default | 0.24 | opacity/gentle |
| status=Default, state=Hover | 0.64 | opacity/strong |
| status=Default, state=Pressed | 1 | opacity/solid |
| status=Default, state=Disabled | 1 | opacity/solid |
| status=Default, state=Focus | 0.24 | opacity/gentle |
root.cursor1
| status=Default, state=Disabled | default | Figma frame size |
root.opacity1
| status=Default, state=Disabled | 0.24 | opacity/gentle |