Theming
View Source
UI kit 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 UI kit 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 '@planning-center/ui-kit'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,},},}
UI kit 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: {backgroundColor: 'purple-5',color: 'white',},outline: {stroke: 'purple-3',color: 'purple-9',},naked: {color: 'purple-3',},},},},}
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 general from '@planningcenter/icons/paths/general'const theme = {icons: {calendar,general,},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 },},}
In order to apply your theme to the type system we must augment or override the default system that ships with UI kit. 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 UI kit, 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 '@planning-center/ui-kit'declare module '@planning-center/ui-kit' {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 UI kit’s ThemeProvider
:
import React from 'react'import { ThemeProvider } from '@planning-center/ui-kit'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.