Nav
Overview
The Nav is a navigation component used for building horizontal and vertical link lists such as navbars, sidebars, and tab bars. It is composed of two recipe parts: useNavRecipe() for the container that controls layout direction and spacing, and useNavItemRecipe() for individual navigation links with color, variant, and interactive state options. Each composable creates a fully configured recipe with compound variants that handle the color-variant combinations automatically.
The Nav 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 Nav recipe?
The Nav recipe helps you:
- Ship faster with sensible defaults: Get 2 orientations, 3 colors, 2 visual styles, and 3 sizes out of the box with a pair of composable calls.
- Compose flexible layouts: Two coordinated recipes (container + item) share the size axis, so your navigation stays internally consistent.
- Maintain consistency: Compound variants ensure every color-variant combination follows the same design rules, including hover, focus, active, and dark mode states.
- 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, or size values at compile time.
- Integrate with your tokens: Every value references the design tokens preset, so theme changes propagate automatically.
Usage
Register the recipes
Add the Nav 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 { useNavRecipe, useNavItemRecipe } from '@styleframe/theme';
const s = styleframe();
const nav = useNavRecipe(s);
const navItem = useNavItemRecipe(s);
export default s;
Build the component
Import the nav and navItem runtime functions from the virtual module and pass variant props to compute class names:
import { nav, navItem } from "virtual:styleframe";
interface NavItemProps {
color?: "light" | "dark" | "neutral";
variant?: "ghost" | "link";
size?: "sm" | "md" | "lg";
active?: boolean;
disabled?: boolean;
href?: string;
children?: React.ReactNode;
}
interface NavProps {
orientation?: "horizontal" | "vertical";
size?: "sm" | "md" | "lg";
children?: React.ReactNode;
}
export function Nav({
orientation = "horizontal",
size = "md",
children,
}: NavProps) {
return (
<nav className={nav({ orientation, size })}>
{children}
</nav>
);
}
export function NavItem({
color = "neutral",
variant = "ghost",
size = "md",
active = false,
disabled = false,
href,
children,
}: NavItemProps) {
return (
<a
href={href}
className={navItem({
color,
variant,
size,
active: active ? "true" : "false",
disabled: disabled ? "true" : "false",
})}
aria-disabled={disabled || undefined}
tabIndex={disabled ? -1 : undefined}
>
{children}
</a>
);
}
<script setup lang="ts">
import { nav } from "virtual:styleframe";
const {
orientation = "horizontal",
size = "md",
} = defineProps<{
orientation?: "horizontal" | "vertical";
size?: "sm" | "md" | "lg";
}>();
</script>
<template>
<nav :class="nav({ orientation, size })">
<slot />
</nav>
</template>
<script setup lang="ts">
import { navItem } from "virtual:styleframe";
const {
color = "neutral",
variant = "ghost",
size = "md",
active = false,
disabled = false,
href,
} = defineProps<{
color?: "light" | "dark" | "neutral";
variant?: "ghost" | "link";
size?: "sm" | "md" | "lg";
active?: boolean;
disabled?: boolean;
href?: string;
}>();
</script>
<template>
<a
:href="href"
:class="navItem({ color, variant, size, active: active ? 'true' : 'false', disabled: disabled ? 'true' : 'false' })"
:aria-disabled="disabled || undefined"
:tabindex="disabled ? -1 : undefined"
>
<slot />
</a>
</template>
See it in action
Orientation
The Nav container recipe supports two orientations that control the flex layout direction. Horizontal arranges items in a row (ideal for top navbars and tab bars), while vertical stacks items in a column (ideal for sidebars and dropdown menus).
Orientation Reference
| Orientation | Flex Direction | Alignment | Use Case |
|---|---|---|---|
horizontal | row | center | Top-level navbars, tab bars, breadcrumbs |
vertical | column | flex-start | Sidebars, dropdown menus, stacked navigation |
horizontal for top-level navigation and vertical for sidebar navigation. The orientation only affects the container — individual items render the same regardless of direction.Colors
The Nav item recipe includes 3 color variants: light, dark, and neutral. Like the Card recipe, Nav uses neutral-spectrum colors designed for structural navigation elements rather than status communication. 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.
The neutral color adapts automatically: it uses dark text in light mode and light text in dark mode, making it the safest default for general-purpose navigation.
Color Reference
| Color | Token | Use Case |
|---|---|---|
light | @color.text / @color.gray-* | Navigation on light backgrounds, stays light-text in dark mode |
dark | @color.gray-200 | Navigation on dark backgrounds, stays dark appearance in light mode |
neutral | Adaptive (light ↔ dark) | Default color, adapts to the current color scheme |
neutral as your default nav item color. It adapts automatically to the user's color scheme, so you don't need to manage light and dark variants separately.Variants
Two visual style variants control how nav items are rendered. Each variant is combined with the selected color through compound variants, so you always get the correct text color and hover behavior for your chosen color.
Ghost
Transparent background that reveals a tinted background on hover. The most common style for navigation, ideal for navbars and sidebars where items should be subtle at rest but clearly interactive on hover.
Link
Transparent background with colored text that gains an underline on hover. Use for secondary navigation, inline link lists, or when items should look like standard hyperlinks.
Sizes
Three size variants from sm to lg control the font size, gap, and padding of the navigation. The size prop affects both the container (font size and gap between items) and individual items (font size and padding).
Size Reference
Nav Container:
| Size | Font Size | Gap |
|---|---|---|
sm | @font-size.xs | @0.25 |
md | @font-size.sm | @0.5 |
lg | @font-size.md | @0.75 |
Nav Item:
| Size | Font Size | Padding (V / H) |
|---|---|---|
sm | @font-size.xs | @0.25 / @0.5 |
md | @font-size.sm | @0.375 / @0.75 |
lg | @font-size.md | @0.5 / @1 |
size prop must be passed to both the nav container and each nav item individually. The container controls font size and gap between items, while items control their own padding.Active
The Nav item recipe includes an active boolean variant. Active items receive font-weight: semibold to visually distinguish the current page or section from other navigation links.
// Active item via variant prop
navItem({ color: "neutral", variant: "ghost", size: "md", active: "true" })
aria-current="page" so both sighted users and screen reader users know which page is currently active.Disabled
The Nav item recipe includes a built-in disabled state through two mechanisms: the &:disabled pseudo-class for native <button> elements, and the disabled boolean variant for <a> elements. Both reduce opacity to 0.5, set cursor: not-allowed, and disable pointer events.
// Disabled item via variant prop
navItem({ color: "neutral", variant: "ghost", size: "md", disabled: "true" })
Anatomy
The Nav recipe is composed of two independent recipes that work together to form a navigation layout:
| Part | Recipe | Role |
|---|---|---|
| Container | useNavRecipe() | Outer wrapper with flex layout, orientation, and gap |
| Item | useNavItemRecipe() | Individual navigation link with color, variant, and interactive states |
Each part is a standalone recipe with its own set of variants. The size prop should be passed consistently to both the container and each item so that font sizes and spacing stay coordinated. The color and variant props only apply to items.
<!-- Both parts working together -->
<nav class="nav(...)">
<a class="navItem(...)">Home</a>
<a class="navItem({ active: 'true' })" aria-current="page">About</a>
<a class="navItem({ disabled: 'true' })" aria-disabled="true">Disabled</a>
</nav>
Accessibility
- Use semantic HTML. Render the container as a
<nav>element with anaria-labelto identify the navigation landmark. Use<a>elements for items that navigate and<button>for items that trigger actions.
<!-- Correct: semantic nav landmark with label -->
<nav aria-label="Main navigation" class="...">
<a href="/" class="...">Home</a>
<a href="/about" class="navItem({ active: 'true' })" aria-current="page">About</a>
</nav>
<!-- Avoid: non-semantic container -->
<div class="...">
<span class="..." onclick="navigate('/')">Home</span>
</div>
- Mark the current page with
aria-current="page". When a nav item represents the active page, addaria-current="page"alongside theactive: "true"variant so screen readers announce it as the current location (WCAG 2.4.8). - Focus visibility. The nav item recipe includes a
:focus-visibleoutline (2px solid, primary color, 2px offset) that appears only during keyboard navigation (WCAG 2.4.7). - Disabled state. For
<a>elements, passdisabled: "true"and addaria-disabled="true"andtabindex="-1"to remove the link from the tab order. The:disabledpseudo-class handles<button>elements automatically.
<!-- Correct: disabled link with ARIA attributes -->
<a href="#" class="navItem({ disabled: 'true' })" aria-disabled="true" tabindex="-1">Disabled</a>
<!-- Correct: disabled button element -->
<button class="..." disabled>Disabled</button>
<nav> elements, give each one a distinct aria-label (e.g., "Main navigation", "Footer navigation") so screen reader users can tell them apart.Customization
Overriding Defaults
Each nav 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 { useNavRecipe, useNavItemRecipe } from '@styleframe/theme';
const s = styleframe();
const nav = useNavRecipe(s, {
base: {
gap: '@1',
},
defaultVariants: {
orientation: 'vertical',
size: 'lg',
},
});
const navItem = useNavItemRecipe(s, {
base: {
borderRadius: '@border-radius.full',
},
defaultVariants: {
color: 'neutral',
variant: 'ghost',
size: 'lg',
},
});
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 { useNavRecipe, useNavItemRecipe } from '@styleframe/theme';
const s = styleframe();
const nav = useNavRecipe(s);
// Only generate neutral color with ghost style
const navItem = useNavItemRecipe(s, {
filter: {
color: ['neutral'],
variant: ['ghost'],
},
});
export default s;
API Reference
useNavRecipe(s, options?)
Creates the nav container recipe with flex layout, orientation, and gap styling.
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 nav container |
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 |
|---|---|---|
orientation | horizontal, vertical | horizontal |
size | sm, md, lg | md |
useNavItemRecipe(s, options?)
Creates the nav item recipe with color, variant, interactive states, and active/disabled boolean variants.
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 nav item |
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 | light, dark, neutral | neutral |
variant | ghost, link | ghost |
size | sm, md, lg | md |
active | true, false | false |
disabled | true, false | false |
Best Practices
- Pass
sizeconsistently to both recipes: The container controls gap and font size, while items control their own padding. Mismatched sizes create visual inconsistency. - Use
neutralfor general-purpose navigation: The neutral color adapts to light and dark mode automatically, making it the safest default. - Prefer
ghostfor primary navigation bars: The transparent resting state keeps the nav clean while hover states confirm interactivity. Uselinkfor secondary or inline navigation. - Always mark the current page as active: Pass
active: "true"alongsidearia-current="page"so both visual and assistive technology users know where they are. - Use
horizontalfor top navbars,verticalfor sidebars: Match the orientation to the layout context rather than mixing orientations within the same navigation level. - Filter what you don't need: If your component only uses one variant, 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
light, dark, and neutral to provide surface-appropriate text colors without implying a specific status.ghost reveals a tinted background on hover (like a toolbar button), while link adds an underline on hover (like a hyperlink). Use ghost for primary navigation bars and link for secondary or inline navigation lists.active boolean variant ("true" / "false"). It sets font-weight: semibold to visually distinguish the current page. Pass active: "true" as a variant prop and always pair it with aria-current="page" for accessibility.<a> elements do not support the native disabled attribute. The Nav item recipe provides a disabled boolean variant that applies the same styles as the :disabled pseudo-class (reduced opacity, not-allowed cursor, no pointer events). Pass disabled: "true" as a variant prop and pair it with aria-disabled="true" and tabindex="-1" to ensure the link is also inaccessible to keyboard and assistive technology users.The Nav item recipe uses compound variants to map each color-variant combination to specific styles. For example, when color is neutral and variant is ghost, the compound variant applies color: @color.text in light mode and color: @color.gray-200 in dark mode, along with hover, focus, and active state overrides. This approach keeps the individual color and variant definitions clean while handling all 6 combinations (3 colors × 2 variants) automatically.
filter option, compound variants that reference filtered-out values are automatically removed. For example, if you filter variant to only ['ghost'], all compound variants matching link are excluded from the generated output. Default variants are also adjusted if they reference a removed value.@color.primary, @font-size.sm, and @border-radius.md 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.Card
A structured container component for grouping related content with header, body, and footer sections. 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.