Custom components
Use this guide after theme configuration when you need markup beyond Cookbook’s built-in surfaces. For ordinary visual changes, consult the component styles reference first.
Import styling tools from @tenphi/cookbook/styling. They use the same Tasty
runtime as Cookbook, with its semantic colors, typography presets, units, and
responsive states. The renderer package also exposes them from
@tenphi/renderer/styling.
| Export | Purpose |
|---|---|
defineComponent |
Create a named Tasty component with its theme.customStyles[name] overrides. |
extendComponent |
Extend an existing Tasty component with a new name and its own theme overrides. |
Button |
Shared button foundation used by Cookbook controls and custom button components. |
tasty |
Use Tasty directly without binding a component to theme.customStyles. |
useGlobalStyles |
Collect a global Tasty style tree while rendering a page. |
resolveComponentStyles |
Merge theme.customStyles[name] into a complete base style tree for global rules. |
mergeStyles |
Compose your own base styles with Tasty’s deep-merge semantics. |
Styles |
TypeScript type for a Tasty style object. |
Cookbook supplies the React renderer and extracts these styles into its static
CSS. No additional Tasty dependency, Astro integration, or client:* directive
is needed for static components. Set shared tokens, presets, and states in
docs.config.ts before rendering. Import the styling entry point in component
modules; configuration files should import defineDocsConfig() from
@tenphi/cookbook/config.
For a component used by your pages, give its root a name and define its parts inside the same style object:
// src/components/StatusBadge.ts
import { defineComponent } from "@tenphi/cookbook/styling";
export const StatusBadge = defineComponent("StatusBadge", {
as: "span",
styles: {
display: "inline-flex",
padding: "0.5x 1x",
radius: "$radius",
fill: "#surface-2",
color: "#text",
preset: "small",
Label: { $: "> strong", preset: "strong" },
},
});Use <StatusBadge><strong>Stable</strong></StatusBadge> in a local MDX page.
A site can change just one part in docs.config.ts:
theme: {
customStyles: {
StatusBadge: { Label: { color: "#accent-text" } },
},
}theme.customStyles supplies a partial style object. defineComponent()
merges it with the base styles while rendering on the server. Document the
root name and every named part when sharing a custom component.
Set site.logo once to use your artwork in the header and mobile drawer:
site: {
title: "Acme",
logo: "./assets/acme.svg"
}Paths are local files relative to the resolved project root, just like
site.favicon. Cookbook reads the intrinsic dimensions and copies the image to
a content-hashed URL under the deployment base. SVG, PNG, JPEG, WebP, AVIF, and
GIF are supported. Missing or invalid files fail during setup.
site: {
title: "Acme",
logo: {
light: "./assets/acme-light.svg",
dark: "./assets/acme-dark.svg",
alt: "Acme documentation",
href: "/",
decorative: true
},
favicon: "./assets/acme-icon.svg"
}Use src for one image, or both light and dark. Variants follow the
appearance control and the system preference in Auto mode. They must share an
aspect ratio to prevent layout shifts. Optional width and height describe
intrinsic dimensions; Tasty controls display size. Customize
theme.styles.SiteLogo and its complete anatomy: Image, Light, and Dark.
Wide wordmarks retain their proportions within the header.
The logo is decorative by default because the neighboring title labels the
site. Set decorative: false and alt when the image itself carries meaningful
text. The header logo link still has a name when the image is decorative.
href defaults to the localized documentation home; it accepts a root-relative
documentation route or HTTP(S) destination. The header title and drawer use the
same destination. site.logo: false removes the mark from both locations.
A logo does not change the favicon or touch icons. Configure site.favicon
separately for square artwork; the default Cookbook icon set remains in place
until changed. Logo remains available as the standalone Cookbook book mark.
The built-in mark uses theme.palette["logo-surface"] and theme.palette["logo-mark"].
The default homepage hero artwork also uses logo-surface for its brand fill.
Both default to Glaze mode: "fixed": the brand background and light book keep
their polarity in dark mode while respecting Glaze’s tone boundaries and
high-contrast settings. The mark-to-background contrast floor is 3:1 normally
and 4.5:1 in high contrast. These colors are separate from interactive accents.
For example, to customize the logo background while retaining brand hue:
theme: {
brand: { from: "#315efb" },
palette: {
"logo-surface": {
base: "logo-mark",
tone: 40,
saturation: 0.8,
mode: "fixed",
contrast: { wcag: [3, 4.5] },
},
"logo-mark": { tone: 100, saturation: 0, mode: "fixed" },
},
}Custom image assets keep their authored colors; use light/dark files when
you want distinct artwork. Header and drawer home links center logos with the
site title regardless of their aspect ratio. Use theme.styles.SiteLogo for
image size and theme.styles.Header.SiteTitle / Sidebar.HomeLink
for typography; they do not need baseline offsets for a different preset.
To put your own logo and title in one home link, create
docs/components/site-title.ts:
import { defineComponent } from "@tenphi/cookbook/styling";
export const SiteTitleRoot = defineComponent("ProjectSiteTitle", {
as: "a",
styles: {
display: "flex",
alignItems: "center",
gap: "1x",
inlineSize: "min 0",
color: "#text",
preset: { "": "h4 / strong", "@mobile": "h5 / strong" },
textDecoration: "none",
Logo: {
$: "> svg",
display: "block",
flexShrink: "0",
inlineSize: { "": "2rem", "@mobile": "1.75rem" },
blockSize: { "": "2rem", "@mobile": "1.75rem" },
color: "#accent-text",
},
Label: {
$: "> span",
inlineSize: "min 0",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
},
},
});Then create docs/components/SiteTitle.astro, using an SVG whose paths use
currentColor to follow the semantic accent color:
---
import ProjectLogo from "../../public/logo.svg";
import { SiteTitleRoot } from "./site-title.js";
const { siteTitle, siteTitleHref } = Astro.locals.cookbookRoute;
---
<SiteTitleRoot href={siteTitleHref}>
<ProjectLogo aria-hidden="true" focusable="false" />
<span translate="no">{siteTitle}</span>
</SiteTitleRoot>Register the replacement in docs.config.ts and disable the shared mark.
A SiteTitle override affects the header; use site.logo above for ordinary
branding shared with the drawer. For custom drawer markup, override Sidebar.
The custom component supports root styles plus its complete list of named
sub-elements: Logo and Label.
site: { logo: false },
components: {
overrides: {
SiteTitle: "./docs/components/SiteTitle.astro"
}
},
theme: {
customStyles: {
ProjectSiteTitle: { Logo: { color: "#text" } }
}
}defineComponent(name, options) accepts Tasty factory options and a name for
theme.customStyles[name]. It merges the configured partial style object before
creating the component, retaining the other base properties and responsive
states. Tasty options such as elements, variants, styleProps, modProps,
and tokenProps keep their behavior and inferred types, including generated
subcomponents such as Component.Label.
Configured styles become the component’s defaults; variants and styles passed
at render time follow Tasty’s usual precedence.
Use defineComponent when authoring a component that should support
theme.customStyles. Use tasty directly for ordinary Tasty creation or composition
that does not need a configuration name. To define a named component around
an existing React component that forwards className, pass it as as in the options. Use className
when passing a class to a Tasty component from Astro.
Use extendComponent(name, base, options) to inherit an existing Tasty
component’s styles, props, variants, and compound parts:
import { Button, extendComponent } from "@tenphi/cookbook/styling";
export const ProjectButton = extendComponent("ProjectButton", Button, {
type: "button",
styles: {
padding: "1x 2x",
radius: "$radius",
fill: "#surface",
Hover: { $: "&:hover", fill: "#surface-2-hover" },
},
});
export const CompactButton = extendComponent("CompactButton", ProjectButton, {
styles: { padding: "0.5x 1x" },
});theme.styles.Button customizes the shared foundation, including Cookbook’s
search and mobile-menu buttons. theme.customStyles.ProjectButton and
CompactButton customize each derived component independently. Button has
no named sub-elements; this example adds Hover to both descendants.
The merge order is the base component’s configured styles and selected variant,
the extension’s styles, its named theme override, and styles passed while
rendering. Tasty merges named sub-elements and state maps using its usual
extending and replacing rules.
Changing a descendant never mutates its base or siblings. If a base exposes
parts such as Button.Label, the descendant exposes those same components.
Extension options accept styles and default values for existing props. Define
new elements, variants, styleProps, modProps, tokenProps, or an as
type on the original factory; Tasty’s wrapping API does not install new factory
settings. Existing required props remain required in the derived component’s
type. To compose reusable style fragments without a component base, use
mergeStyles(baseStyles, extensionStyles); a shallow spread replaces entire
nested sub-elements and state maps.
For markup you do not render through a Tasty component, call
useGlobalStyles() during rendering. For example, this Astro component
supports root styles and a Label sub-element through theme.customStyles.ProjectNote:
---
import {
resolveComponentStyles,
useGlobalStyles,
} from "@tenphi/cookbook/styling";
useGlobalStyles(
".project-note",
resolveComponentStyles("ProjectNote", {
padding: "2x",
fill: "#surface-2",
Label: { $: "> strong", color: "#text", preset: "strong" },
}),
);
---
<aside class="project-note"><strong>Note</strong><slot /></aside>defineComponent, extendComponent, and resolveComponentStyles read the same theme.customStyles
configuration; user configuration contains only the properties to change. Each custom style name must match a call to one of these helpers.
@tenphi/cookbook/eslint-plugin re-exports the Tasty ESLint plugin together with
Cookbook’s validationConfig. The renderer offers the same exports from
@tenphi/renderer/eslint-plugin. Keep style definitions in .ts or .tsx
modules, as in the site title example above, to lint them with either ESLint or
oxlint.
Create tasty.config.ts at your project root:
export default {
extends: "@tenphi/cookbook",
};Renderer-only consumers can use extends: "@tenphi/renderer" instead.
The preset registers both Cookbook styling import paths, its built-in tokens,
units, responsive states, and typography presets. It also describes
defineComponent, extendComponent, resolveComponentStyles, and mergeStyles, so their inline
style objects, variants, and named sub-elements receive the same checks as
tasty() calls. Partial overrides passed to mergeStyles may omit default state
values and retain the plugin’s safeguards against fixes that replace base styles.
Install ESLint and a TypeScript parser:
pnpm add -D eslint @typescript-eslint/parserAdd the plugin to eslint.config.mjs:
import parser from "@typescript-eslint/parser";
import tasty from "@tenphi/cookbook/eslint-plugin";
export default [
{
...tasty.configs.recommended,
files: ["**/*.{ts,tsx}"],
languageOptions: { parser },
},
];Run pnpm exec eslint docs/components --max-warnings 0. Use
tasty.configs.strict for additional checks, including custom property names and
runtime style values. The named recommended and strict exports contain rule
maps for configurations that register the plugin themselves.
Install oxlint:
pnpm add -D oxlintCreate oxlint.config.mjs using its JavaScript plugin support:
import { recommended } from "@tenphi/cookbook/eslint-plugin";
export default {
jsPlugins: [{ name: "tasty", specifier: "@tenphi/cookbook/eslint-plugin" }],
rules: recommended,
};Run pnpm exec oxlint --config oxlint.config.mjs docs/components --deny-warnings.
The tasty alias keeps rule names such as tasty/known-property consistent with
ESLint. Import strict instead of recommended to enable the stricter rule map.
Both presets are tested with oxlint 1.83.0; JavaScript plugin support is experimental.
Add names introduced by your docs.config.ts theme to the validation config.
For example, after defining theme.tokens["$project-gap"], theme.states["@project-wide"],
and theme.presets["project-title"]:
import type { TastyValidationConfig } from "@tenphi/cookbook/eslint-plugin";
export default {
extends: "@tenphi/cookbook",
tokens: ["$project-gap"],
states: ["@project-wide"],
presets: ["project-title"],
} satisfies TastyValidationConfig;extends merges and deduplicates these arrays with the inherited configuration,
so list only your additions. It also inherits the styling helper signatures;
entries you add to styleFunctions override inherited signatures by function name.
The exported validationConfig object remains available for programmatic use.
The entry point also exports the upstream StyleFunctionConfig and
ResolvedConfig types for shared lint configurations. Additional imported
helpers can be registered in styleFunctions; use partial: true for helpers
that merge overrides into existing styles.