Composables

Chip

A status indicator component positioned at the corner of an element. Supports multiple colors, visual styles, sizes, positions, and inset mode through the recipe system.

Overview

The Chip is a small status indicator element positioned at the corner of another element, commonly used for notification badges, unread counts, and online status dots. It is composed of two recipe parts: useChipRecipe() for the positioning wrapper and useChipIndicatorRecipe() for the indicator itself. Each composable creates a fully configured recipe with color, variant, size, position, and inset options — plus compound variants that handle the color-variant combinations automatically.

The Chip recipes integrate directly with the default design tokens preset and generate type-safe utility classes at build time with zero runtime CSS.

Why use the Chip recipe?

The Chip recipe helps you:

  • Ship faster with sensible defaults: Get 9 colors, 2 visual styles, 5 sizes, 4 positions, and an inset mode out of the box with two composable calls.
  • Compose structured layouts: Two coordinated recipes (wrapper and indicator) work together so the indicator is always positioned correctly relative to the wrapped element.
  • Maintain consistency: Compound variants ensure every color-variant combination follows the same design rules, including dark mode overrides.
  • Customize without forking: Override base styles, default variants, or filter out options you don't need — all through the options API.
  • Stay type-safe: Full TypeScript support means your editor catches invalid color, variant, size, or position values at compile time.
  • Integrate with your tokens: Every value references the design tokens preset, so theme changes propagate automatically.
  • Support dark mode: Background and text colors adapt automatically between light and dark color schemes.

Usage

Register the recipes

Add the Chip recipes to a local Styleframe instance. The global styleframe.config.ts provides design tokens and utilities, while the component-level file registers the recipes themselves:

src/components/chip.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useChipRecipe, useChipIndicatorRecipe } from '@styleframe/theme';

const s = styleframe();

const chip = useChipRecipe(s);
const chipIndicator = useChipIndicatorRecipe(s);

export default s;

Build the component

Import the chip and chipIndicator runtime functions from the virtual module and pass variant props to compute class names:

src/components/Chip.tsx
import { chip, chipIndicator } from "virtual:styleframe";

interface ChipProps {
    color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "light" | "dark" | "neutral";
    variant?: "solid" | "soft";
    size?: "xs" | "sm" | "md" | "lg" | "xl";
    position?: "top-right" | "top-left" | "bottom-right" | "bottom-left";
    inset?: boolean;
    text?: string;
    children?: React.ReactNode;
}

export function Chip({
    color = "primary",
    variant = "solid",
    size = "md",
    position = "top-right",
    inset = false,
    text,
    children,
}: ChipProps) {
    return (
        <div className={chip()}>
            {children}
            <span
                className={chipIndicator({
                    color,
                    variant,
                    size,
                    position,
                    inset: String(inset),
                })}
            >
                {text}
            </span>
        </div>
    );
}

See it in action

Colors

The Chip indicator recipe includes 9 color variants: 6 semantic colors (primary, secondary, success, info, warning, error) plus 3 neutral-scale colors (light, dark, neutral). Each color is combined with every visual style variant through compound variants, so you get consistent, predictable styling across all combinations — including dark mode overrides.

Color Reference

ColorTokenUse Case
primary@color.primaryDefault. Primary brand indicator, unread counts
secondary@color.secondarySecondary or supporting indicators
success@color.successOnline status, positive states, completions
info@color.infoInformational badges, tips
warning@color.warningCaution indicators, pending states
error@color.errorError badges, alert counts
light@color.white / @color.gray-*Light surfaces, stays light in dark mode
dark@color.gray-900Dark surfaces, stays dark in light mode
neutralAdaptive (light ↔ dark)Adapts to the current color scheme
Pro tip: Use semantic color names that describe purpose, not appearance. This makes it easier to update your palette without touching component code.

Variants

Two visual style variants control how the indicator is rendered. Each variant is combined with the selected color through compound variants, so you always get the correct background and text colors for your chosen color.

Solid

Filled background with light text. The most prominent style, ideal for notification counts and status indicators that need high visibility.

Soft

Light tinted background with colored text. A subtler style that works well for secondary indicators and badges that shouldn't dominate the visual hierarchy. In dark mode, the tint and text colors automatically adjust.

Sizes

Five size variants from xs to xl control the dimensions, font size, and padding of the indicator. The xs size produces a small dot with no text, while larger sizes accommodate text content.

Size Reference

SizeDimensionsFont SizeUse Case
xs@0.375 × @0.375 (fixed)@font-size.4xsStatus dots, no text
sm@0.75 min-width × @0.75@font-size.3xsSmall count badges
md@1 min-width × @1@font-size.2xsDefault. Standard notification badges
lg@1.25 min-width × @1.25@font-size.xsProminent indicators
xl@1.5 min-width × @1.5@font-size.smLarge badges with longer text
Good to know: The xs size uses fixed width and height (no min-width), creating a perfect circle dot. Sizes sm through xl use min-width with horizontal padding, so the indicator grows horizontally to fit text content.

Position

The position variant controls which corner of the parent element the indicator is placed at. The indicator is absolutely positioned and uses CSS transform to offset itself to straddle the corner.

PositionPlacementTransform
top-rightTop-right cornertranslate(50%, -50%)
top-leftTop-left cornertranslate(-50%, -50%)
bottom-rightBottom-right cornertranslate(50%, 50%)
bottom-leftBottom-left cornertranslate(-50%, 50%)

Inset

The inset variant controls whether the indicator overlaps the corner of the parent element or sits inside it. When inset is true, the transform offset is removed so the indicator stays within the parent's bounds.

InsetBehavior
falseDefault. Indicator straddles the corner, offset by 50% of its own size
trueIndicator sits inside the corner with no offset (translate(0, 0))

With Text

Chip indicators can display text content such as notification counts. Sizes sm through xl include horizontal padding and min-width to accommodate text while maintaining a pill shape thanks to the full border radius.

Anatomy

The Chip recipe is composed of two independent recipes that work together:

PartRecipeRole
WrapperuseChipRecipe()Outer container with position: relative and display: inline-flex to establish the positioning context
IndicatoruseChipIndicatorRecipe()Absolutely positioned badge with color, size, position, and inset styling

The wrapper provides the positioning context. The indicator is a child element that positions itself at one of the four corners using absolute positioning and transforms.

<!-- Both parts working together -->
<div class="chip()">
    <img src="avatar.png" alt="User avatar" />
    <span class="chipIndicator({ color: 'success', size: 'xs' })"></span>
</div>
Pro tip: The wrapper recipe has no variants — it only establishes position: relative and display: inline-flex. You can apply it to any element that needs a chip indicator without affecting its visual appearance.

Accessibility

  • Use semantic HTML. Use a <span> for the indicator element. It is a visual decoration that augments the wrapped element.
<!-- Correct: indicator as a span -->
<div class="chip()">
    <button>Notifications</button>
    <span class="chipIndicator(...)">5</span>
</div>
  • Add aria-label for screen readers. The indicator's text content alone may not provide enough context. Add an aria-label to the wrapped element or use aria-describedby to associate the count with the action.
<div class="chip()">
    <button aria-label="Notifications, 5 unread">Notifications</button>
    <span class="chipIndicator(...)" aria-hidden="true">5</span>
</div>
  • Hide decorative indicators. When the indicator is a status dot without text (e.g., xs size), add aria-hidden="true" to prevent screen readers from announcing an empty element.
<!-- Status dot: hidden from screen readers -->
<div class="chip()">
    <img src="avatar.png" alt="User avatar, online" />
    <span class="chipIndicator({ size: 'xs', color: 'success' })" aria-hidden="true"></span>
</div>

Customization

Overriding Defaults

Each chip composable accepts an optional second argument to override any part of the recipe configuration. Overrides are deep-merged with the defaults, so you only need to specify the properties you want to change:

src/components/chip.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useChipRecipe, useChipIndicatorRecipe } from '@styleframe/theme';

const s = styleframe();

const chip = useChipRecipe(s);
const chipIndicator = useChipIndicatorRecipe(s, {
    defaultVariants: {
        color: 'success',
        variant: 'solid',
        size: 'xs',
        position: 'bottom-right',
        inset: 'false',
    },
});

export default s;

Filtering Variants

If you only need a subset of the available variants, use the filter option to limit which values are generated. This reduces the output CSS and keeps your component API focused:

src/components/chip.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useChipRecipe, useChipIndicatorRecipe } from '@styleframe/theme';

const s = styleframe();

// Only generate primary and error colors, with solid style
const chip = useChipRecipe(s);
const chipIndicator = useChipIndicatorRecipe(s, {
    filter: {
        color: ['primary', 'error'],
        variant: ['solid'],
    },
});

export default s;
Good to know: Filtering also removes compound variants and adjusts default variants that reference filtered-out values, so your recipe stays consistent.

API Reference

useChipRecipe(s, options?)

Creates the chip wrapper recipe that establishes the positioning context for the indicator.

Parameters:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles for the wrapper

The wrapper recipe has no variants. It sets position: relative and display: inline-flex as base styles.

useChipIndicatorRecipe(s, options?)

Creates the chip indicator recipe with color, variant, size, position, and inset support.

Parameters:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles for the indicator
options.variantsVariantsCustom variant definitions for the recipe
options.defaultVariantsRecord<keyof Variants, string>Default variant values for the recipe
options.compoundVariantsCompoundVariant[]Custom compound variant definitions for the recipe
options.filterRecord<string, string[]>Limit which variant values are generated

Variants:

VariantOptionsDefault
colorprimary, secondary, success, info, warning, error, light, dark, neutralprimary
variantsolid, softsolid
sizexs, sm, md, lg, xlmd
positiontop-right, top-left, bottom-right, bottom-lefttop-right
insettrue, falsefalse

Learn more about recipes →

Best Practices

  • Choose colors by meaning, not appearance: Use success for online status and error for alert counts — this keeps your UI consistent when tokens change.
  • Use xs for status dots: The smallest size creates a perfect circle without text, ideal for online/offline indicators.
  • Use solid for high-visibility badges: Solid indicators stand out against most backgrounds. Use soft for subtler, less urgent indicators.
  • Match position to reading direction: top-right (default) works well for LTR layouts. Consider top-left for RTL layouts.
  • Use inset for tight layouts: When the indicator shouldn't overflow the parent's bounds, enable inset mode to keep it inside the corner.
  • Filter what you don't need: If your component only uses a few colors or sizes, pass a filter option to reduce generated CSS.
  • Override defaults at the recipe level: Set your most common variant combination as defaultVariants so component consumers write less code.

FAQ