Theming
Tapestry-React ships with sensible defaults for all themeable properties that can be fully overridden.
The ThemeProvider
component is responsible for overriding themeable values throughout a tree of Tapestry-React components.
Provide a custom theme object with the respective themeable properties you would like to change. Note that your theme will be merged with the default theme, so you only need to override or add themeable values that matter to your use case.
import { StackView, Text, ThemeProvider } from '@planningcenter/tapestry-react'const theme = {boxSizes: {small: { paddingHorizontal: 1, paddingHorizontal: 0.75 },medium: { paddingHorizontal: 1.5, paddingHorizontal: 1 },},breakpoints: {phone: 480,tablet: 720,desktop: 960,},colors: {primary: 'hotpink',secondary: 'lime',},}function App() {return (<ThemeProvider theme={theme}><StackView axis="horizontal" spacing={0.5}><Text color="primary">Hello</Text><Text color="secondary">World</Text></StackView></ThemeProvider>)}render(App)
Box sizes are used for Avatar, Badge, Button, Input, and StepperField components.
const theme = {boxSizes: {sm: {boxSize: 3,fontSize: 5,lineHeight: 2.5,paddingHorizontalDense: 0.5,paddingHorizontal: 1,paddingVertical: 0.25,radius: 3,},md: {boxSize: 4,fontSize: 4,lineHeight: 3,paddingHorizontalDense: 1,paddingHorizontal: 1.375,paddingVertical: 0.5,radius: 4,},},}
Tapestry-React uses Planning Center breakpoints by default. See how breakpoints are defined and customized in the responsive guide.
Button themes must provide all 3 variants fill
, outline
, and naked
for each theme. See what themes are provided by default.
A simple button theme that overrides the primary
theme might look like the following:
const theme = {button: {themes: {primary: {fill: {active: { backgroundColor: 'blue-5' },backgroundColor: 'green-5',color: 'white',disabled: {backgroundColor: 'grey-3',color: 'grey-5'},focus: { backgroundColor: 'blue-6'},focusVisible: {outlineColor: 'blue-4',outlineOffset: '2px',outlineStyle: 'solid',outlineWidth: '2px'},hover: { backgroundColor: 'green-4' },},}}},}
Colors can be fully customized or integrated with your existing system through CSS custom properties. See how colors can be defined in the colors guide.
Allows modifying the following properties for Checkbox:
const theme = {checkbox: {fill: 'surfaceTertiary',stroke: 'separatorSecondary',focusStroke: 'blue-5',checkedFill: 'primary-light',checkedStroke: 'primary',},}
Icons can be fully customized or integrated with your existing system. The only requirement is that you only export path data. See Icon for an example of using custom sets of icons with ThemeProvider
.
import * as calendar from '@planningcenter/icons/paths/calendar'import * as giving from '@planningcenter/icons/paths/giving'const theme = {icons: {calendar,giving,},icon: {viewBox: '0 0 16 16',},}
Theme keys for most Page components.
const theme = {pageBody: { backgroundColor: 'backgroundSecondary' },pageButton: { theme: 'white' },pageDropdown: { theme: 'white', variant: 'outline' },pageHeader: { backgroundColor: 'primary-light' },pageTitle: { level: 2, color: 'white' },}
Allows modifying the following properties for Radio:
const theme = {radio: {fill: 'surfaceTertiary',stroke: 'separatorSecondary',focusStroke: 'blue-5',checkedFill: 'primary-light',checkedStroke: 'primary',},}
Spinner sizes and thicknesses can be customized by assigning an object with new values to the size keys of xxs-xxl
:
const theme = {spinner: {sizes: { xxs: 1, xs: 2, sm: 3, md: 4, lg: 5, xl: 6, xxl: 7 },thickness: { xxs: 2, xs: 2, sm: 2, md: 3, lg: 4, xl: 6, xxl: 8 },},}
ToggleSwitch background color can be customized by adding a backgroundColor
key to the toggleSwitch
object:
const theme = {toggleSwitch: {backgroundColor: 'primary',},}
The following keys can be used in ThemeProvider
for customizing the related component’s default properties.
Theme key
Component
badge
combobox
dateField
drawer
editActions
field
fieldSet
HeadingUppercase
highlight
inputLabel
modal
pagination
popover
progress
rangeSlider
scrim
section
segmentedControl
segmentedTabs
spinner
stepperField
stepperProgress
tabNav
tabNav.tab
tabs
text
toggleSwitch
tooltip
wizard
To use, add the key for the desired component into the theme
object that is passed to the ThemeProvider
.
// change default values for `<Badge />` using `badge` keyconst theme = {badge: {color: 'primary-lightest',fontWeight: 300,radius: 'pill',size: 'sm',},}
In order to apply your theme to the type system we must augment or override the default system that ships with Tapestry-React. If you don’t have a definitions file yet, create a global.d.ts
file at the root of your project. Now we’ll declare a module type to override Tapestry-React, add your specific types that correlate to your ThemeProvider
values here:
Note that Breakpoints
must be defined in your project in order to get autocompletion for the mediaQueries prop.
import '@planningcenter/tapestry-react'declare module '@planningcenter/tapestry-react' {export interface Breakpoints {mobile: numbertablet: numberdesktop: number}export interface Colors {'purple-0': string'purple-1': string'purple-2': string'purple-3': string'purple-4': string'purple-5': string'purple-6': string'purple-7': string'purple-8': string'purple-9': string}}
You should now have rich autocompletion using your theme settings. Note that in VS Code specifically, if you don’t see the type information update you can restart the TypeScript server by opening the command palette and selecting TypeScript: Restart TS server
.
In a React Rails environment there can be multiple roots that React is rendered into. This poses an issue when needing to provide a common component like ThemeProvider
to every component. We’ll look at two different ways we can overwrite the react-rails render method.
First, we’ll create an AppProvider
component that makes use of Tapestry-React’s ThemeProvider
:
import React from 'react'import { ThemeProvider } from '@planningcenter/tapestry-react'function AppProvider({ children }) {const organizationStore = Services.Flux.getStore('OrganizationCurrentStore')const theme = {calendar: {weekStartsOn: organizationStore? organizationStore.weekStartsOnSunday()? 0: 1: 0,},}return <ThemeProvider theme={theme}>{children}</ThemeProvider>}export default AppProvider
Next, if we want to overwrite the render method in JavaScript we can do this in our application.js
file or wherever our JavaScript is initiated.
Add the following snippet to override all of our root components and wrap them with our AppProvider
:
import AppProvider from './AppProvider'// modified from https://github.com/reactjs/react-rails/blob/master/react_ujs/index.js#L83-L121window.ReactRailsUJS.mountComponents = (searchSelector) => {let ujs = window.ReactRailsUJSlet nodes = ujs.findDOMNodes(searchSelector)for (let i = 0; i < nodes.length; ++i) {let node = nodes[i]let className = node.getAttribute(ujs.CLASS_NAME_ATTR)let constructor = ujs.getConstructor(className)let propsJson = node.getAttribute(ujs.PROPS_ATTR)let props = propsJson && JSON.parse(propsJson)let hydrate = node.getAttribute(ujs.RENDER_ATTR)let cacheId = node.getAttribute(ujs.CACHE_ID_ATTR)let turbolinksPermanent = node.hasAttribute(ujs.TURBOLINKS_PERMANENT_ATTR)let shouldTransformProps = node.getAttribute('transform_props') === 'true'if (!constructor) {let message = "Cannot find component: '" + className + "'"if (console && console.log) {console.log('%c[react-rails] %c' + message + ' for element','font-weight: bold','',node)}throw new Error(message + '. Make sure your component is available to render.')} else {let component = components[cacheId]if (component === undefined) {component = React.createElement(constructor,shouldTransformProps ? transformProps(props) : props)if (turbolinksPermanent) {components[cacheId] = component}}if (hydrate && typeof ReactDOM.hydrate === 'function') {ReactDOM.hydrate(<AppProvider>{component}</AppProvider>, node)} else {ReactDOM.render(<AppProvider>{component}</AppProvider>, node)}}}}
Or if we want to overwrite the view helper we can create our own that utilizes our AppProvider
component:
in lib/react_mounter.rb
class ReactMounter < React::Rails::ComponentMountdef react_component(name, props = {}, options = {}, &block)props[:component] = namehtml_tag = superhtml_tag.sub!(name, 'AppProvider')html_tag.html_safeendend
in config/application.rb
# ...require_relative '../lib/react_mounter'# ...module Peopleclass Application < Rails::Application# ...config.react.view_helper_implementation = ::ReactMounter# ...end
Please note that require.context needs to be set up properly in order to resolve the component correctly.