Context lets a parent component share state with deeply nested children without passing props through every level. It is the foundation of compound component patterns like Dialog, Accordion, and Tabs.
import { createContext, useContext } from '@barefootjs/dom'#`createContext`
Creates a new context with an optional default value.
const MyContext = createContext<T>(defaultValue?: T)Type:
type Context<T> = {
readonly id: symbol
readonly defaultValue: T | undefined
readonly Provider: (props: { value: T; children?: unknown }) => unknown
}#`Context.Provider`
Provides a value to all descendant components. Any component inside the provider tree can read the value with useContext.
<MyContext.Provider value={someValue}>
{props.children}
</MyContext.Provider>The compiler transforms <Context.Provider> into an internal provideContext() call. At runtime, the value is set synchronously before children initialize, so useContext always sees the provided value.
#`useContext`
Reads the current value from a context.
const value = useContext(MyContext)Behavior:
- If a
Providerancestor exists, returns the provided value - If no
Providerexists and a default value was passed tocreateContext, returns the default - If no
Providerexists and no default was set, throws an error
#Basic Example
"use client"
import { createContext, useContext } from '@barefootjs/dom'
// 1. Create the context
const ThemeContext = createContext<'light' | 'dark'>('light')
// 2. Provider component
export function ThemeProvider(props: { theme: 'light' | 'dark'; children?: Child }) {
return (
<ThemeContext.Provider value={props.theme}>
{props.children}
</ThemeContext.Provider>
)
}
// 3. Consumer component
export function ThemedButton(props: { children?: Child }) {
const handleMount = (el: HTMLButtonElement) => {
const theme = useContext(ThemeContext)
el.className = theme === 'dark' ? 'btn-dark' : 'btn-light'
}
return <button ref={handleMount}>{props.children}</button>
}// Usage
<ThemeProvider theme="dark">
<ThemedButton>Click me</ThemedButton> {/* Gets dark styling */}
</ThemeProvider>#Compound Components
Context is most commonly used for compound components — a group of related components that share internal state. The root component provides the state; sub-components consume it.
#Example: Accordion
"use client"
import { createSignal, createContext, useContext, createEffect } from '@barefootjs/dom'
// Context type
interface AccordionContextValue {
activeItem: () => string | null
toggle: (id: string) => void
}
// Create context
const AccordionContext = createContext<AccordionContextValue>()
// Root component — provides state
function Accordion(props: { children?: Child }) {
const [activeItem, setActiveItem] = createSignal<string | null>(null)
const toggle = (id: string) => {
setActiveItem(prev => prev === id ? null : id)
}
return (
<AccordionContext.Provider value={{ activeItem, toggle }}>
<div data-slot="accordion">{props.children}</div>
</AccordionContext.Provider>
)
}
// Trigger — toggles the active item
function AccordionTrigger(props: { itemId: string; children?: Child }) {
const handleMount = (el: HTMLButtonElement) => {
const ctx = useContext(AccordionContext)
el.addEventListener('click', () => {
ctx.toggle(props.itemId)
})
createEffect(() => {
const isOpen = ctx.activeItem() === props.itemId
el.setAttribute('aria-expanded', String(isOpen))
})
}
return <button ref={handleMount}>{props.children}</button>
}
// Content — shows/hides based on active item
function AccordionContent(props: { itemId: string; children?: Child }) {
const handleMount = (el: HTMLElement) => {
const ctx = useContext(AccordionContext)
createEffect(() => {
const isOpen = ctx.activeItem() === props.itemId
el.hidden = !isOpen
})
}
return <div ref={handleMount}>{props.children}</div>
}Usage:
<Accordion>
<AccordionTrigger itemId="faq-1">What is BarefootJS?</AccordionTrigger>
<AccordionContent itemId="faq-1">
<p>A JSX-to-template compiler with signal-based reactivity.</p>
</AccordionContent>
<AccordionTrigger itemId="faq-2">How does hydration work?</AccordionTrigger>
<AccordionContent itemId="faq-2">
<p>Marker-driven: bf-* attributes tell the client JS where to attach.</p>
</AccordionContent>
</Accordion>#Reactive Context Values
Context values can contain signal getters. When a child component reads a signal getter from context inside a createEffect, the effect re-runs when the signal changes:
// Provider passes signal getter
<AccordionContext.Provider value={{ activeItem, toggle }}>// Consumer reads inside createEffect — reactive
const ctx = useContext(AccordionContext)
createEffect(() => {
const isOpen = ctx.activeItem() === props.itemId // Tracks activeItem signal
el.hidden = !isOpen
})The signal getter ctx.activeItem() is called inside the effect, so the effect subscribes to activeItem. When activeItem changes (via toggle), only the affected effects re-run.
#Context Without a Default
When createContext is called without a default value, useContext throws if no Provider ancestor is found. This is the recommended pattern for compound components where a provider is always expected:
const DialogContext = createContext<DialogContextValue>()
// If DialogTrigger is used outside a Dialog, useContext throws
function DialogTrigger(props: { children?: Child }) {
const handleMount = (el: HTMLElement) => {
const ctx = useContext(DialogContext) // Throws if no Dialog ancestor
// ...
}
return <button ref={handleMount}>{props.children}</button>
}This catches composition errors early. If a sub-component is accidentally used outside its parent, the error message identifies the missing provider.
#Context With a Default
When a default is provided, useContext always succeeds:
const ThemeContext = createContext<'light' | 'dark'>('light')
// Works even without a ThemeProvider ancestor — returns 'light'
const theme = useContext(ThemeContext)Use this pattern for optional contexts where a sensible fallback exists.