stackable-components
v1.4.2
Published
Reusable React components for Contentstack projects
Readme
Stackable Components
Ready-made React section components for Contentstack projects — heroes, banners, grids, pricing tables, FAQs, footers and more. Drop them onto a page, feed them entry data, and they render.
Installation
npm install stackable-componentsInstall the peer dependencies too, if your project doesn't already have them:
npm install react react-dom @heroicons/react react-iconsQuick start
import { HeroBanner, ProductCard } from 'stackable-components';
import 'stackable-components/dist/styles.css'; // required
export default function MyPage() {
return (
<>
<HeroBanner
heading="Welcome to Our Site"
subheading="Discover amazing products"
cta_button={{ text: 'Get Started', link: '/products' }}
/>
<ProductCard
product_name="Amazing Product"
description="This product will change your life"
price={99.99}
/>
</>
);
}You don't need Tailwind. dist/styles.css is a self-contained build that carries every utility the components use. If your project already runs Tailwind, importing it alongside is fine.
Every prop is optional and has a sensible default, so a component renders something reasonable before any content reaches it.
Runtime renderer
A site doesn't have to import components one by one. StackablesEntry renders any
entry straight from its content type's schema, so one route can serve every page of
a Stackables-published stack:
import { StackablesEntry, parseSiteConfig, pageConfigFor, referencePaths } from 'stackable-components';
// entry: the fetched entry (references resolved — see referencePaths(schema))
// schema: the entry's content type schema, with global field schemas included
// config: the `stackables_site_config` entry Stackables publishes into the stack
<StackablesEntry entry={entry} schema={contentType.schema} config={config} page={pageConfigFor(config, contentType.uid)} />A global_field field renders as one component, a reference field as one per
referenced entry, a group as a run of sibling sections and a blocks field as one
component per block, in schema order. Asset objects are flattened to their URLs.
Which component a content type / global field uid maps to comes from the site
config's components map (falling back to the uid in PascalCase); per-section widths
and the page's container cap come from its pages entry. StackablesChrome renders
the site header / footer the same way from the config's header / footer, and
readTypography + themeFromTypography turn a composed Typography component into a
ThemeProvider theme.
A site can supply its own components: components (by library name) replaces a
library component wherever it appears; componentsByUid (by content type / global
field uid) renders every field pointing at that uid — including uids the library has no
component for — and, given the entry's own contentType, can take a whole page type
over. renderUnknown is called for anything nothing renders.
Custom pages (1.2.1)
A page type made directly in Contentstack — one Stackables never designed, so the site config says nothing about it — routes and renders too:
buildRoutes(config, contentTypes)gives every content type with URLs a route after the site config's pages, from its URL Prefix joined to its URL Pattern (contentTypeUrlPattern: prefix/newpage/+ pattern/:title→/newpage/:title;{title}is accepted as a placeholder too). Such routes havecustom: trueandpage: null. Where one shares a URL shape with a designed page, the designed page is tried first.StackablesEntryrenders a custom page's component fields as usual, and its plain fields — text, rich text (HTML or JSON), Markdown, files, links, numbers, dates, booleans — as a document: consecutive plain fields share one section in the container, styled by the.sk-plainrules instyles.css. A custom page with no component fields also gets its title as an<h1>. This isrenderPlainFields, which defaults to on exactly when nopageis given; a page designed in Stackables renders precisely as before.
Markdown fields are parsed by the library itself (renderer/markdown.tsx, no
dependency) straight to React elements — CommonMark's everyday set plus GitHub's
tables, task lists, strikethrough and bare links; raw HTML in the text is shown
as text, never interpreted. A boolean shows with its label ("Gift wrapping:
Yes" / "No"; .sk-plain__boolean), and nothing when the entry never set it.
A multi-entry reference field made in Contentstack (field_metadata.ref_multiple,
with multiple left false) renders every entry it holds, not only the first —
isMultipleReference(field) is the test, exported alongside the others.
Field overrides — StackablesEntry's fields prop replaces the rendering of any
plain field, the kinds the library never renders (custom, json, taxonomy)
included:
<StackablesEntry
fields={{
uids: { care_notes: CareCard, "details.size_chart": SizeChart, internal_notes: null },
customFields: { blt0123456789abcdef: StarRating }, // by custom field extension uid
types: { markdown: ArticleMarkdown, boolean: YesNoBadge },
}}
/* … */
/>Checked most specific first: uids[path], uids[uid], customFields[extension uid],
types[kind]; null hides a field; uids.title replaces a custom page's title
heading. types apply where plain fields render (a custom page); uids and
customFields on every page. An override receives { field, value, kind, path,
entry, parent, editTag, children }, children being the library's own rendering.
Kinds: text, multiline, markdown, html, json_rte, number, date, file,
link, boolean, select, custom, json, taxonomy (fieldKind(field) tells
which). A select shows its choice's label; custom fields, plain JSON and taxonomies
render only through an override.
PlainField, isPlainField and hasComponentFields are exported for a site that
takes a custom page over but wants its text rendered the same way. Add .sk-plain
to an element for the typography alone; the automatic sections also carry
.sk-plain--section for their padding.
Every content component is reachable by name through the registry
(getComponent('HeroBanner')) — withSectionWidth registers each export under the
display name it is given, which is why that name must match the catalog's
react_component.
Server code (a Next.js generateMetadata, say) should import the pure helpers from
stackable-components/runtime instead — same functions, no components in the bundle,
so it loads inside a React Server Component where the main entry cannot.
Available Components
Typography
- Typography: Themed text primitive for headings/body copy
Hero Banners
- HeroBanner: Full-width gradient hero with CTA button
- SplitHeroBanner: Split layout hero with content and image placeholder
- HeroBannerClassic: Classic hero with heading lines, avatars, and footer stats
- SpotlightHero: Hero with a spotlighted CTA list
Banners
- Banner / BannerLight: Full-width promo banners (dark/light)
- BannerCallout: Inline callout banner
- PromoBanner: Rotating multi-slide promo banner
- CommunityBanner: Banner with avatar stack and CTA
- CountdownBanner: Banner with a countdown timer
- CookieConsentBanner: Cookie consent bar
Carousels & Sliders
- ImageVideoCarousel: Interactive image/video carousel with navigation
- CircleImageCarousel: Circular image carousel
- VerticalProductCarousel: Vertical scrolling product carousel
- GalleryCarousel: Gallery-style slide carousel
- ProductSlider: Product slide carousel with CTA
Product Components
- ProductCard / ProductCardRegular / ProductCardLarge: Product display cards
- ProductBanner: Banner-style product highlight
- ProductListing: Grid layout for multiple products (PLP)
- ProductDetails: Product detail page layout (PDP)
Cards
- FeatureCard: Feature highlight card with icon
- ResourceCard: Resource/article card list
- ProfileCard: Profile card with stats
- FlipCard: Card with a flip animation
- CardAccordion: Expandable accordion card
Grids
- BentoGrid: Bento-style mixed grid layout
- IndexSection: Indexed list/grid section
- HexTileGrid: Hexagonal tile grid
- CategoryGrid: Category tile grid
- FlipCardGrid: Grid of flip cards
- ImageGrid / ImageGridDynamic: Static and dynamic image grids
Testimonials & Reviews
- Testimonial: Testimonial quote block
- RatingSummary: Aggregate rating summary
- CardStack: Stacked testimonial cards
- Reviews: Review list section
Logo Cloud
- LogoCloud: Scrolling/static logo strip
FAQ
- FAQ: Standard FAQ accordion
- FaqCategorized: FAQ grouped by category
Empty State
- EmptyState: Empty/placeholder state block
Pricing
- Pricing: Pricing tier comparison
- PricingSpreadCard: Single spread pricing card
Statistics
- StatCounterRow: Row of animated stat counters
Forms
- ContactForm: Customizable contact form
- ContactFormSplit: Split-layout contact form
- NewsletterSignup: Newsletter signup form
Navigation
- NavigationBar: Responsive navigation with mobile menu
- ExtendedNavigation: Multi-level navigation menu
- BreadcrumbNavigation: Breadcrumb trail
- Sidebar: Sidebar navigation
- FloatingDock: Floating dock/quick-links nav
Footers
- FooterSimple: Minimal single-row footer
- FooterMultiColumn: Multi-column footer with links
CTAs
- TextCTA: Text-led call-to-action section
- JustifiedTextCTA: Justified-text call-to-action section
Buttons
- PrimaryButton: Primary action button
- ToggleSwitch: On/off toggle control
Article Pages
- ArticleDetails: Article detail page layout
- ArticleListing: Article listing page layout
Tabs
- Tabs: Tabbed content section
Section Introduction
- SectionIntroduction: Section heading/intro block
- SectionIntroductionTwoColumn: Two-column section intro
Video
- VideoSection: Heading block with a video that plays in place — a video file or a YouTube, Vimeo or Loom link
Feature Benefits
- FeatureBenefitStrip: Compact benefit strip
- FeatureBenefit / FeatureBenefitDark: Benefit list sections (light/dark)
- FeatureBenefitWithImage: Benefit section with supporting image
- FeatureBenefitRenderer: Variant-driven benefit renderer
- StandardBenefit: Configurable standard benefits section
Feature Showcase
- StickyScrollFeature: Sticky scroll-driven feature showcase
Timeline
- VerticalTimeline / TimelineHorizontal / TimelineColumn: Timeline layouts
- TimelineYearBlocks: Year-grouped timeline blocks
Feature Comparison
- FeatureComparisonTable: Tabular feature comparison
- FeatureComparisonVs: Head-to-head comparison
- FeatureComparisonCards: Card-based comparison
Team
- TeamBanner: Team member showcase banner
Social
- SocialPostEmbed: Embedded social post
- SocialLinks: Social media link list
Process
- ProcessStep: Step-by-step process section
Awards
- AwardsSection: Awards/recognition showcase
Auth
- AuthSplitScreen: Split-screen auth (sign in/up) layout
Page width
Components sit inside a page container that the page owns, not the component. Each section picks one of three modes via container_width:
| mode | section box | content inside |
|---|---|---|
| inherit (default) | stops at the container | stops at the container |
| full_background | full page width — the background bleeds edge to edge | stops at the container |
| full | full page width | full page width |
Set the container once for the page with PageLayout:
import { PageLayout, HeroBanner, FAQ, StatCounterRow } from 'stackable-components';
<PageLayout maxWidth={1280}>
<HeroBanner container_width="full" {...heroProps} />
<FAQ container_width="full_background" {...faqProps} />
<StatCounterRow container_width="inherit" {...statsProps} />
</PageLayout>Or skip PageLayout and pass container_cap per component:
<FAQ container_width="full_background" container_cap={1280} {...props} />container_cap accepts a number (px), a CSS length ('80rem'), or 'full' for no cap. The container width is always a ceiling — it can pull content in, never push it out, so a 1280px container never overflows a 390px phone. Default is 1280px.
Add this to your global stylesheet so no section can ever widen the page:
html, body { max-width: 100%; overflow-x: hidden; }Theming
Brand colours, fonts and button styling come from CSS variables. Set them with ThemeProvider:
import { ThemeProvider } from 'stackable-components';
<ThemeProvider theme={{ primary: '#e11d48', containerWidth: '1280px', fontFamily: 'Inter, sans-serif' }}>
{children}
</ThemeProvider>Or set the variables yourself in CSS — --stackables-primary, --stackables-container-width, --stackables-font-family, and the --stackables-btn-* family.
Contentstack Live Preview
Every component accepts a $ prop carrying Contentstack's edit tags, one per editable field, so entries stay click-to-edit in Live Preview:
<FAQ heading={entry.heading} faqs={entry.faqs} $={entry.$} />TypeScript
Full type definitions ship with the package:
import type { HeroBannerProps, SectionWidth, StackablesTheme } from 'stackable-components';Each component exports its own props interface (FaqProps, PricingProps, …), and every component additionally accepts container_width and container_cap.
License
MIT — a Contentstack Solutions project.
