React Components

Style and customize the React components

Updated 37 views

The components come with one optional stylesheet and four ways to change them, from a colour to full control over the markup. Each level leaves the others working, so you can restyle one card and keep the defaults everywhere else.

The stylesheets

ts
import '@kookee/react/content.css'; // the components
import '@kookee/sdk/styles/content.css'; // code blocks and file chips inside entry bodies

Without @kookee/react/content.css the components render unstyled, with every class name and data-* attribute still in place. Apart from declaring its variables on :root, the stylesheet targets only kookee- classes, never bare elements, and contains no reset, so it cannot restyle the rest of your site.

@kookee/react/content.css styles the text of the entry bodies the components render, which carry the kookee-typography class: headings, links, lists, quotes, code, images and tables, with --kookee-body-size setting the text size. A standalone EntryContent is left to your own typography, such as Tailwind's prose; give it className="kookee-typography" for the same styles. For an entry body you render without the components, @kookee/sdk/styles/typography.css gives baseline styles.

Tailwind v4

Import both stylesheets into the components layer, after Tailwind, so the utilities you pass through className and classNames win over the defaults:

CSS
@import 'tailwindcss';
@import '@kookee/sdk/styles/content.css' layer(components);
@import '@kookee/react/content.css' layer(components);

The order matters. Imported before tailwindcss, the components layer is declared below Preflight, whose rules for h1 to h6, ol and img then override the components' own.

Give the lists a width

BlogPostList, ChangelogList, HelpCategoryGrid and HelpArticleList are size containers (container-type: inline-size), so their layout follows their own width, not the window's. A size container takes no width from its content, so give it a definite one: a block, a stretched flex child, or a 1fr grid track. As a shrink-to-fit child, such as a flex item without a width, w-fit or an auto grid track, the list collapses to zero width.

Level 1: CSS variables

Set the theme with variables:

CSS
:root {
  --kookee-primary: #0f766e;
  --kookee-primary-text: #0f766e; /* active and pressed text */
  --kookee-radius: 8px;
  --kookee-card-min: 20rem; /* minimum card width in the grids */
}

Load these after the stylesheet, or keep them unlayered when the stylesheet sits in a layer, because the stylesheet declares its own defaults on :root too.

Variable

What it sets

--kookee-primary

accent colour of links, active items and buttons

--kookee-primary-soft

the light accent tint behind hovered, pressed and active items, and behind search matches

--kookee-primary-text

accent-coloured text; lighter in dark mode, so it stays readable

--kookee-bg, --kookee-bg-secondary

card and page-section backgrounds

--kookee-text, --kookee-text-secondary

main and muted text

--kookee-border, --kookee-error

borders, and error text

--kookee-radius, --kookee-radius-sm

corner radius of cards and small elements

--kookee-shadow, --kookee-shadow-sm, --kookee-focus-ring, --kookee-transition

shadows, the keyboard focus ring and hover transitions

--kookee-card-min, --kookee-gap, --kookee-cover-ratio

grid card width, spacing between items, and the aspect ratio of covers

--kookee-popover-z

z-index of the what's-new popover

--kookee-type-feature … --kookee-type-other

one badge colour per changelog release type

--kookee-announcement-info … --kookee-announcement-new-feature

one colour per announcement type (-info, -warning, -critical, -promotion, -maintenance, -new-feature) for its pill, row, icon and badge. Unset, info takes --kookee-info, warning and maintenance --kookee-warn, critical --kookee-danger, promotion --kookee-primary-text, new feature --kookee-success

--kookee-column-color

set by each roadmap column from the colour you gave it

The React chat widget reads the same variable names. When it is on the same page, set your values on the components' roots instead of :root, so the chat keeps its own look. On a root, also set the two variables derived from the accent, because the defaults are computed from the :root value:

CSS
.kookee-blog-list,
.kookee-toc {
  --kookee-primary: var(--brand);
  --kookee-primary-text: var(--brand);
  --kookee-primary-soft: color-mix(in srgb, var(--brand) 12%, transparent);
  --kookee-focus-ring: 0 0 0 3px color-mix(in srgb, var(--brand) 22%, transparent);
}

Dark mode

Without a palette of your own, add the kookee-dark class to any ancestor for dark, or kookee-auto to follow the visitor's operating system:

tsx
<html className={theme === 'dark' ? 'dark kookee-dark' : undefined}>

With your own palette, map the variables to your design tokens, which already switch with your dark mode, and leave kookee-dark out:

CSS
:root {
  --kookee-bg: var(--color-background);
  --kookee-text: var(--color-foreground);
  --kookee-text-secondary: var(--color-muted-foreground);
  --kookee-border: var(--color-border);
  --kookee-primary: var(--color-primary);
  --kookee-primary-text: var(--color-primary);
}

Do one or the other. Mixed, your :root mapping wins over the stylesheet's .kookee-dark block once the stylesheet sits in a layer, so kookee-dark changes only the variables you did not map.

Level 2: classes

Every component takes className for its root and classNames for its inner elements, called slots. Lists also take itemClassNames, applied to every item, and BlogPost, ChangelogArticle and HelpArticle take headerClassNames for the inner elements of their header. A few more slots reach inside the parts: titleLink, the link inside a card's title, on every card; coverImage, the image inside a cover, on blog cards, changelog items and the blog and changelog headers; coverPlaceholder on blog cards; and authorAvatar and authorName on the changelog header:

tsx
<ChangelogList
  entries={entries}
  className="my-12"
  classNames={{ list: 'divide-y' }}
  itemClassNames={{ title: 'text-xl', titleLink: 'no-underline', badge: 'uppercase' }}
/>

Or write CSS against the stable class names. A slot's class is the root's class plus the slot name, so the title of a blog card is kookee-blog-card-title:

CSS
.kookee-blog-card-title {
  font-size: 1.25rem;
}

Each default rule is a single class, so one class of yours, loaded after the stylesheet, is enough to override it, in every state.

Level 3: structure

When the layout itself has to change:

  • Parts. Every default layout is built from exported parts, such as BlogPostTitle, BlogPostCover and ChangelogTypeBadge. Compose them in a different order or leave some out.

  • renderItem replaces each item of a list with your own markup, usually built from those parts. The list still wraps each item in its <li>.

  • children replaces the default layout inside a card (BlogPostCard, ChangelogItem, HelpCategoryCard, HelpArticleCard, RoadmapPostCard), the text of the roadmap's title and excerpt parts, and the label of the WhatsNew button, next to its count.

  • renderLink swaps the default <a> for your router's link component.

  • renderImage swaps the default <img> for next/image or your image CDN.

Title parts link only the title unless you pass stretched, which makes the nearest positioned ancestor clickable as a whole; the default cards do that.

Every component also forwards ref and native attributes, such as id, aria-* and data-testid, to its root element. Where a component sets role, aria-label or aria-current itself, its own value wins; rename those through labels.

Level 4: headless hooks

The interactive components are built on hooks you can use with markup of your own: useReactions, useActiveHeading, useWhatsNew and useHelpSearchBox. They keep the behaviour, the state and the requests, and render nothing. Their options and results are described in the article for each area.

State as data attributes

State is exposed as data-* attributes, so it can be styled without JavaScript. Attributes that are true or false are always present, as "true" or "false", so select [data-active='true'], not [data-active].

  • data-type on changelog items, type badges, type filter items, and reaction and vote buttons.

  • data-type on announcement pills, rows and articles and on their type badges and icons: info, warning, critical, promotion, maintenance or newFeature, with info for a missing or unknown type. data-openable on AnnouncementPill and data-selectable on announcement rows, "true" when the title is a button.

  • data-has-link on cards and changelog items, and data-has-cover on blog cards and changelog items.

  • data-active on tag filter, type filter, roadmap category filter and category navigation items and on table of contents links, "true" on the active one. data-depth on table of contents items and links, data-page on pagination items.

  • data-state (open or closed) and data-has-unseen on WhatsNew, and data-unseen on its items.

  • data-status (idle, loading, success or error) and data-empty on HelpSearchBox.

  • data-pressed on reaction and vote buttons; data-pending and data-error on Reactions and HelpfulVote; data-vote (yes, no or none) on HelpfulVote.

  • data-column-id, data-column-type and data-column-name on roadmap columns, and data-category on roadmap cards, category badges and category filter items.

  • data-expanded on a BlogTagFilter with a limit, data-overflow="true" on the tags past it, and data-placeholder="true" on a cover box showing its placeholder. These three are set only where they apply.

  • data-skeleton and aria-busy on skeletons.

Labels and translation

Every visible text comes from a labels object, one per area: BlogLabels, ChangelogLabels, HelpLabels, EntryLabels, RoadmapLabels and AnnouncementLabels. Each component of an area takes it as labels and uses its own keys, so a translated site builds one object per area and passes it everywhere.

Counted labels take plural forms, resolved for locale:

tsx
<HelpCategoryGrid
  categories={categories}
  locale="ru"
  labels={{ articleCount: { one: '{count} статья', few: '{count} статьи', many: '{count} статей', other: '{count} статьи' } }}
/>

Labels

Keys, with their English defaults

EntryLabels

tocTitle (On this page; an empty string hides it), tocNavigation (Table of contents), paginationNavigation (Pagination), paginationPrevious (Previous), paginationNext (Next), paginationPage (Page {page}), reactionsGroup (Reactions), reactionFire (Fire), reactionHeart (Love), reactionRocket (Rocket), reactionEyes (Eyes), reactionMindblown (Mind blown)

BlogLabels

empty (No posts yet.), tagFilter (Filter posts by tag), allTags (All posts)

ChangelogLabels

empty (No updates yet.), versionPrefix (empty), feature (Feature), fix (Fix), improvement (Improvement), breaking (Breaking), security (Security), deprecated (Deprecated), other (Update), typeFilter (Filter updates by type), allTypes (All), whatsNewTitle (What's new), whatsNewUnseen ({count} new updates, plural), whatsNewNew (New), whatsNewViewAll (View all updates), whatsNewEmpty (No updates yet.)

HelpLabels

noCategories (No categories yet.), noArticles (No articles yet.), articleCount ({count} articles, plural), lastUpdated (Updated {date}), breadcrumbs (Breadcrumb), breadcrumbsHelpCenter (Help Center), categoryNavigation (Categories), helpfulQuestion (Was this article helpful?), helpfulYes (Yes), helpfulNo (No), searchLabel (Search help articles), searchPlaceholder (Search for answers…), searchLoading (Searching…), searchError (Search failed. Please try again.), searchNoResults (No articles found.), searchResultCount ({count} articles found, plural), views ({count} views, plural)

RoadmapLabels

empty (Nothing on the roadmap yet.), emptyColumn (Nothing here yet.), feature (Feature), improvement (Improvement), bug (Bug), other (Other), votes ({count} votes, plural), comments ({count} comments, plural), categoryFilter (Filter posts by category), allCategories (All)

AnnouncementLabels

info (Info), warning (Warning), critical (Critical), promotion (Promotion), maintenance (Maintenance), newFeature (New feature), empty (No announcements), dismiss (Dismiss)

Dates

Dates are absolute, such as Sep 15, 2026, and computed in UTC, so the server's HTML and the browser's first render agree. locale changes the format. formatDate replaces it entirely, for example with relative dates your app computes after hydration.

Slots reference

root is always the component's own class; each slot adds the root class plus -<slot>.

Component

Root class

Slots

EntryContent

kookee-entry-content

none

TableOfContents, TrackedTableOfContents

kookee-toc

title, list, item, link

Pagination

kookee-pagination

list, item, link, previous, next, ellipsis

Reactions

kookee-reactions

button, icon, label, count

BlogPostList

kookee-blog-list

list, item, empty

BlogPostCard

kookee-blog-card

cover, body, title, excerpt, meta, author, date, tags, titleLink, coverImage, coverPlaceholder

BlogPost

kookee-blog-post

header, content

BlogPostHeader

kookee-blog-post-header

tags, title, meta, author, date, cover, coverImage

BlogTagFilter

kookee-blog-tag-filter

list, item, link, name, count, toggle

ChangelogList

kookee-changelog-list

list, item, empty

ChangelogItem

kookee-changelog-item

meta, date, version, body, heading, title, badge, summary, cover, titleLink, coverImage

ChangelogArticle

kookee-changelog-article

header, content

ChangelogArticleHeader

kookee-changelog-article-header

meta, date, version, badge, author, title, cover, coverImage, authorAvatar, authorName

ChangelogTypeFilter

kookee-changelog-type-filter

list, item, link

WhatsNew

kookee-whats-new

trigger, label, badge, panel, title, list, item, date, link, new, empty, all

HelpCategoryGrid

kookee-help-category-grid

list, item, empty

HelpCategoryCard

kookee-help-category-card

icon, title, description, count, titleLink

HelpArticleList

kookee-help-article-list

list, item, empty

HelpArticleCard

kookee-help-article-card

category, title, snippet, titleLink

HelpArticle

kookee-help-article

header, content

HelpArticleHeader

kookee-help-article-header

title, updated

HelpCategoryHeader

kookee-help-category-header

title, icon, name, description, count

HelpCategoryNav

kookee-help-category-nav

list, item, link, icon, name, count

HelpBreadcrumbs

kookee-help-breadcrumbs

list, item, link, current, separator

HelpSearchBox

kookee-help-search

label, input, status, list, item

HelpfulVote

kookee-helpful-vote

question, options, button, icon, label, count

Roadmap

kookee-roadmap

empty

RoadmapColumn

kookee-roadmap-column

header, dot, title, count, list, item, empty

RoadmapPostCard

kookee-roadmap-card

title, excerpt, meta, votes, category, comments, titleLink

RoadmapCategoryFilter

kookee-roadmap-category-filter

list, item, link

AnnouncementPill

kookee-announcement-pill

open, icon, title, dismiss

AnnouncementList

kookee-announcement-list

list, item, empty

AnnouncementList rows (itemClassNames)

kookee-announcement-item

icon, body, title, titleButton, excerpt, meta, badge, date, dismiss

AnnouncementArticle

kookee-announcement-article

header, icon, title, meta, badge, date, content

every skeleton

its list's root class (kookee-roadmap for RoadmapSkeleton)

list, item, card, block

titleLink, coverImage, coverPlaceholder, authorAvatar and authorName add no class of their own. RoadmapSkeleton also takes columnClassNames. Parts carry their own class and slots as well, such as kookee-blog-post-title with its link slot, BlogPostCover with image and placeholder, the author parts with avatar and name, or kookee-changelog-type-badge. The marks Highlight renders have kookee-highlight. Other two-word slots get a class in kebab case: titleButton on an announcement row is kookee-announcement-item-title-button. The announcement type badges and icons carry kookee-announcement-type-badge and kookee-announcement-type-icon.

What stays stable

Props, slot names, class names, label keys, data-* attributes, CSS variables and hook results are public, and change only in a major release. How elements are nested inside a component is not, and may change in a minor release. If your CSS depends on a particular structure, compose the exported parts instead of relying on the default nesting.

Continue reading

Was this article helpful?