Chip
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:
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:
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>
);
}
<script setup lang="ts">
import { chip, chipIndicator } from "virtual:styleframe";
const props = withDefaults(
defineProps<{
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;
}>(),
{ color: "primary", variant: "solid", size: "md", position: "top-right", inset: false },
);
</script>
<template>
<div :class="chip()">
<slot />
<span
:class="chipIndicator({
color,
variant,
size,
position,
inset: inset ? 'true' : 'false',
})"
>
{{ text }}
</span>
</div>
</template>
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
| Color | Token | Use Case |
|---|---|---|
primary | @color.primary | Default. Primary brand indicator, unread counts |
secondary | @color.secondary | Secondary or supporting indicators |
success | @color.success | Online status, positive states, completions |
info | @color.info | Informational badges, tips |
warning | @color.warning | Caution indicators, pending states |
error | @color.error | Error badges, alert counts |
light | @color.white / @color.gray-* | Light surfaces, stays light in dark mode |
dark | @color.gray-900 | Dark surfaces, stays dark in light mode |
neutral | Adaptive (light ↔ dark) | Adapts to the current color scheme |
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
| Size | Dimensions | Font Size | Use Case |
|---|---|---|---|
xs | @0.375 × @0.375 (fixed) | @font-size.4xs | Status dots, no text |
sm | @0.75 min-width × @0.75 | @font-size.3xs | Small count badges |
md | @1 min-width × @1 | @font-size.2xs | Default. Standard notification badges |
lg | @1.25 min-width × @1.25 | @font-size.xs | Prominent indicators |
xl | @1.5 min-width × @1.5 | @font-size.sm | Large badges with longer text |
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.
| Position | Placement | Transform |
|---|---|---|
top-right | Top-right corner | translate(50%, -50%) |
top-left | Top-left corner | translate(-50%, -50%) |
bottom-right | Bottom-right corner | translate(50%, 50%) |
bottom-left | Bottom-left corner | translate(-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.
| Inset | Behavior |
|---|---|
false | Default. Indicator straddles the corner, offset by 50% of its own size |
true | Indicator 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:
| Part | Recipe | Role |
|---|---|---|
| Wrapper | useChipRecipe() | Outer container with position: relative and display: inline-flex to establish the positioning context |
| Indicator | useChipIndicatorRecipe() | 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>
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-labelfor screen readers. The indicator's text content alone may not provide enough context. Add anaria-labelto the wrapped element or usearia-describedbyto 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.,
xssize), addaria-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:
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:
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;
API Reference
useChipRecipe(s, options?)
Creates the chip wrapper recipe that establishes the positioning context for the indicator.
Parameters:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom 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:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom base styles for the indicator |
options.variants | Variants | Custom variant definitions for the recipe |
options.defaultVariants | Record<keyof Variants, string> | Default variant values for the recipe |
options.compoundVariants | CompoundVariant[] | Custom compound variant definitions for the recipe |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
color | primary, secondary, success, info, warning, error, light, dark, neutral | primary |
variant | solid, soft | solid |
size | xs, sm, md, lg, xl | md |
position | top-right, top-left, bottom-right, bottom-left | top-right |
inset | true, false | false |
Best Practices
- Choose colors by meaning, not appearance: Use
successfor online status anderrorfor alert counts — this keeps your UI consistent when tokens change. - Use
xsfor status dots: The smallest size creates a perfect circle without text, ideal for online/offline indicators. - Use
solidfor high-visibility badges: Solid indicators stand out against most backgrounds. Usesoftfor subtler, less urgent indicators. - Match position to reading direction:
top-right(default) works well for LTR layouts. Considertop-leftfor RTL layouts. - Use
insetfor 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
filteroption to reduce generated CSS. - Override defaults at the recipe level: Set your most common variant combination as
defaultVariantsso component consumers write less code.
FAQ
position: relative and display: inline-flex to establish the positioning context, while the indicator has color, variant, size, position, and inset variants. Keeping them separate means the wrapper recipe can be applied to any element without adding unnecessary variant classes.top/bottom and left/right CSS properties set to 0. A CSS transform with translate(50%, -50%) (or the appropriate signs for each corner) offsets the indicator so it straddles the corner of the parent element. When inset is true, compound variants override the transform to translate(0, 0), keeping the indicator inside the parent bounds.inset variant uses "true" and "false" as string keys internally, but your component can accept a boolean prop and convert it with String(inset) or a ternary (inset ? 'true' : 'false') when passing it to the recipe function.light always uses light tones regardless of the color scheme. dark always uses darker tones. neutral adapts to the current color scheme: it appears light in light mode and dark in dark mode. Use neutral when you want the indicator to blend naturally with the surrounding interface.filter option, compound variants that reference filtered-out values are automatically removed. For example, if you filter color to only ['primary', 'success'], all compound variants matching secondary, info, warning, error, light, dark, or neutral are excluded from the generated output. Default variants are also adjusted if they reference a removed value.@color.primary, @font-size.2xs, @border-radius.full, and @z-index.base through string refs. These tokens need to be defined in your Styleframe instance for the recipe to generate valid CSS. The easiest way is to use useDesignTokensPreset(s), but you can also define the required tokens manually.Popover
A floating container component with structured sections and a directional arrow for contextual content. Supports multiple colors, visual styles, and sizes through the recipe system.
Overview
Explore Styleframe's utility composables for generating CSS utility classes. Create flexible, reusable styling primitives with full type safety.