Overview
Creating custom components in Paste involves:- Using Paste primitives like Box and Text
- Accessing theme tokens with
useTheme - Leveraging style props for consistency
- Following Paste’s accessibility patterns
Using Box as a Foundation
The Box component is the most flexible primitive for building custom components:import { Box } from '@twilio-paste/core/box';
function CustomCard({ children, variant = 'default' }) {
const isElevated = variant === 'elevated';
return (
<Box
backgroundColor="colorBackgroundBody"
borderRadius="borderRadius30"
padding="space70"
boxShadow={isElevated ? 'shadowCard' : 'none'}
borderWidth="borderWidth10"
borderStyle="solid"
borderColor="colorBorder"
>
{children}
</Box>
);
}
Accessing Theme Values
Use theuseTheme hook to access raw theme values:
import { useTheme } from '@twilio-paste/core/theme';
import { Box } from '@twilio-paste/core/box';
function CustomGradientBox({ children }) {
const theme = useTheme();
return (
<Box
padding="space60"
borderRadius="borderRadius30"
style={{
background: `linear-gradient(135deg, ${
theme.backgroundColors.colorBackgroundPrimary
}, ${
theme.backgroundColors.colorBackgroundPrimaryStrong
})`,
}}
>
{children}
</Box>
);
}
Composing Paste Components
Build complex components by combining existing Paste components:import { Box } from '@twilio-paste/core/box';
import { Text } from '@twilio-paste/core/text';
import { Heading } from '@twilio-paste/core/heading';
import { Button } from '@twilio-paste/core/button';
import { Stack } from '@twilio-paste/core/stack';
function FeatureCard({ title, description, action, onAction }) {
return (
<Box
backgroundColor="colorBackgroundBody"
borderRadius="borderRadius30"
padding="space70"
boxShadow="shadowCard"
>
<Stack orientation="vertical" spacing="space50">
<Heading as="h3" variant="heading30">
{title}
</Heading>
<Text as="p" color="colorTextWeak">
{description}
</Text>
{action && (
<Box>
<Button variant="primary" onClick={onAction}>
{action}
</Button>
</Box>
)}
</Stack>
</Box>
);
}
// Usage
<FeatureCard
title="Get Started"
description="Learn how to integrate Paste into your application"
action="Read Documentation"
onAction={() => console.log('Clicked')}
/>
Creating Styled Components
For more complex styling needs, use the Paste styling library:import { styled } from '@twilio-paste/styling-library';
import { Box } from '@twilio-paste/core/box';
const StyledCard = styled(Box)(({ theme, variant }) => ({
backgroundColor: theme.backgroundColors.colorBackgroundBody,
borderRadius: theme.radii.borderRadius30,
padding: theme.space.space70,
boxShadow: variant === 'elevated'
? theme.shadows.shadowCard
: 'none',
borderWidth: theme.borderWidths.borderWidth10,
borderStyle: 'solid',
borderColor: theme.borderColors.colorBorder,
transition: 'all 0.2s ease',
'&:hover': {
boxShadow: theme.shadows.shadow,
transform: 'translateY(-2px)',
},
}));
function HoverCard({ children, variant }) {
return <StyledCard variant={variant}>{children}</StyledCard>;
}
Advanced Custom Components
Interactive Component with State
import { useState } from 'react';
import { Box } from '@twilio-paste/core/box';
import { Text } from '@twilio-paste/core/text';
import { Stack } from '@twilio-paste/core/stack';
function Tabs({ items, defaultTab = 0 }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<Box>
<Stack orientation="horizontal" spacing="space0">
{items.map((item, index) => (
<Box
key={index}
as="button"
onClick={() => setActiveTab(index)}
paddingX="space60"
paddingY="space40"
backgroundColor={
activeTab === index
? 'colorBackgroundPrimaryWeakest'
: 'transparent'
}
borderBottomWidth="borderWidth20"
borderBottomStyle="solid"
borderBottomColor={
activeTab === index
? 'colorBorderPrimary'
: 'transparent'
}
cursor="pointer"
_hover={{
backgroundColor: 'colorBackgroundPrimaryWeakest',
}}
>
<Text
as="span"
fontWeight={
activeTab === index ? 'fontWeightBold' : 'fontWeightNormal'
}
>
{item.label}
</Text>
</Box>
))}
</Stack>
<Box padding="space70">
{items[activeTab].content}
</Box>
</Box>
);
}
// Usage
<Tabs
items={[
{ label: 'Overview', content: <div>Overview content</div> },
{ label: 'Details', content: <div>Details content</div> },
{ label: 'Settings', content: <div>Settings content</div> },
]}
/>
Component with Variants
import { Box } from '@twilio-paste/core/box';
import { Text } from '@twilio-paste/core/text';
const variantStyles = {
info: {
backgroundColor: 'colorBackgroundNeutralWeakest',
borderColor: 'colorBorderNeutral',
iconColor: 'colorTextNeutral',
},
success: {
backgroundColor: 'colorBackgroundSuccessWeakest',
borderColor: 'colorBorderSuccess',
iconColor: 'colorTextSuccess',
},
warning: {
backgroundColor: 'colorBackgroundWarningWeakest',
borderColor: 'colorBorderWarning',
iconColor: 'colorTextWarning',
},
error: {
backgroundColor: 'colorBackgroundErrorWeakest',
borderColor: 'colorBorderError',
iconColor: 'colorTextError',
},
};
function Callout({ variant = 'info', title, children }) {
const styles = variantStyles[variant];
return (
<Box
backgroundColor={styles.backgroundColor}
borderLeftWidth="borderWidth30"
borderLeftStyle="solid"
borderLeftColor={styles.borderColor}
padding="space60"
borderRadius="borderRadius20"
>
{title && (
<Text
as="div"
fontWeight="fontWeightBold"
color={styles.iconColor}
marginBottom="space30"
>
{title}
</Text>
)}
<Text as="div">{children}</Text>
</Box>
);
}
// Usage
<Callout variant="warning" title="Important">
This action cannot be undone.
</Callout>
Responsive Custom Component
import { Box } from '@twilio-paste/core/box';
import { useTheme } from '@twilio-paste/core/theme';
function ResponsiveGrid({ children }) {
return (
<Box
display="grid"
gridTemplateColumns={[
'repeat(1, 1fr)', // Mobile: 1 column
'repeat(2, 1fr)', // Tablet: 2 columns
'repeat(3, 1fr)', // Desktop: 3 columns
]}
columnGap="space60"
rowGap="space60"
>
{children}
</Box>
);
}
// Usage
<ResponsiveGrid>
<FeatureCard title="Feature 1" />
<FeatureCard title="Feature 2" />
<FeatureCard title="Feature 3" />
<FeatureCard title="Feature 4" />
</ResponsiveGrid>
Using Element Names
Add custom element names to your components for customization:import { Box } from '@twilio-paste/core/box';
function CustomCard({ element = 'CUSTOM_CARD', children }) {
return (
<Box
element={element}
padding="space60"
borderRadius="borderRadius20"
backgroundColor="colorBackgroundBody"
>
{children}
</Box>
);
}
// Allow customization via CustomizationProvider
<CustomizationProvider
elements={{
CUSTOM_CARD: {
padding: 'space90',
boxShadow: 'shadowCard',
},
}}
>
<CustomCard>Customizable card</CustomCard>
</CustomizationProvider>
Forwarding Props
Create flexible components that accept additional props:import { Box } from '@twilio-paste/core/box';
function FlexContainer({ children, direction = 'row', gap = 'space50', ...props }) {
return (
<Box
display="flex"
flexDirection={direction}
gap={gap}
{...props}
>
{children}
</Box>
);
}
// Usage with additional props
<FlexContainer
direction="column"
gap="space70"
padding="space60"
backgroundColor="colorBackgroundBody"
>
<div>Item 1</div>
<div>Item 2</div>
</FlexContainer>
Accessibility Patterns
Keyboard Navigation
import { Box } from '@twilio-paste/core/box';
function AccessibleButton({ children, onClick }) {
const handleKeyPress = (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onClick?.(event);
}
};
return (
<Box
as="button"
onClick={onClick}
onKeyPress={handleKeyPress}
padding="space40"
cursor="pointer"
backgroundColor="colorBackgroundPrimary"
color="colorTextInverse"
borderRadius="borderRadius20"
_focus={{
outline: 'none',
boxShadow: 'shadowFocus',
}}
_hover={{
backgroundColor: 'colorBackgroundPrimaryStrong',
}}
>
{children}
</Box>
);
}
ARIA Attributes
import { Box } from '@twilio-paste/core/box';
import { Text } from '@twilio-paste/core/text';
function StatusBadge({ status, children }) {
const isActive = status === 'active';
return (
<Box
role="status"
aria-live="polite"
display="inline-flex"
alignItems="center"
paddingX="space40"
paddingY="space20"
backgroundColor={
isActive
? 'colorBackgroundSuccessWeakest'
: 'colorBackgroundNeutralWeakest'
}
borderRadius="borderRadiusPill"
>
<Box
width="sizeSquare20"
height="sizeSquare20"
borderRadius="borderRadiusCircle"
backgroundColor={
isActive
? 'colorBackgroundSuccess'
: 'colorBackgroundWeak'
}
marginRight="space30"
aria-hidden="true"
/>
<Text as="span" fontSize="fontSize20">
{children}
</Text>
</Box>
);
}
Animation and Transitions
import { styled } from '@twilio-paste/styling-library';
import { Box } from '@twilio-paste/core/box';
const AnimatedBox = styled(Box)`
transition: all 0.3s ease;
&:hover {
transform: scale(1.05);
}
`;
function AnimatedCard({ children }) {
return (
<AnimatedBox
padding="space60"
backgroundColor="colorBackgroundBody"
borderRadius="borderRadius30"
boxShadow="shadowCard"
>
{children}
</AnimatedBox>
);
}
TypeScript Support
Add proper TypeScript types to your custom components:import { Box } from '@twilio-paste/core/box';
import type { BoxProps } from '@twilio-paste/core/box';
interface CustomCardProps extends Omit<BoxProps, 'padding' | 'borderRadius'> {
variant?: 'default' | 'elevated' | 'outlined';
children: React.ReactNode;
}
function CustomCard({
variant = 'default',
children,
...props
}: CustomCardProps) {
const getVariantStyles = () => {
switch (variant) {
case 'elevated':
return { boxShadow: 'shadowCard' as const };
case 'outlined':
return {
borderWidth: 'borderWidth10' as const,
borderStyle: 'solid' as const,
borderColor: 'colorBorder' as const,
};
default:
return {};
}
};
return (
<Box
padding="space60"
borderRadius="borderRadius30"
backgroundColor="colorBackgroundBody"
{...getVariantStyles()}
{...props}
>
{children}
</Box>
);
}
Best Practices
1. Use Paste Primitives
// Good: Build on Paste primitives
import { Box, Text } from '@twilio-paste/core';
function CustomComponent() {
return (
<Box padding="space50">
<Text>Content</Text>
</Box>
);
}
// Avoid: Raw HTML elements
function CustomComponent() {
return (
<div style={{ padding: '16px' }}>
<span>Content</span>
</div>
);
}
2. Use Theme Tokens
// Good: Reference theme tokens
<Box
padding="space50"
backgroundColor="colorBackgroundBody"
color="colorText"
/>
// Avoid: Hardcoded values
<Box
padding="16px"
backgroundColor="#FFFFFF"
color="#000000"
/>
3. Make Components Customizable
// Good: Accept element prop
function CustomCard({ element = 'CUSTOM_CARD', ...props }) {
return <Box element={element} {...props} />;
}
// Avoid: No customization hooks
function CustomCard(props) {
return <Box {...props} />;
}
4. Follow Accessibility Guidelines
// Good: Proper semantics and ARIA
<Box
as="button"
role="button"
aria-label="Close dialog"
tabIndex={0}
>
Close
</Box>
// Avoid: Poor accessibility
<Box onClick={handleClick}>
Close
</Box>
5. Document Your Components
/**
* FeatureCard displays a feature with title, description, and action.
*
* @param {string} title - The card title
* @param {string} description - The card description
* @param {string} action - The action button text
* @param {Function} onAction - Callback when action is clicked
*
* @example
* <FeatureCard
* title="Get Started"
* description="Learn the basics"
* action="Start Learning"
* onAction={() => navigate('/learn')}
* />
*/
function FeatureCard({ title, description, action, onAction }) {
// Component implementation
}
Common Patterns
Compound Components
function Card({ children }) {
return (
<Box
backgroundColor="colorBackgroundBody"
borderRadius="borderRadius30"
boxShadow="shadowCard"
>
{children}
</Box>
);
}
Card.Header = function CardHeader({ children }) {
return (
<Box
padding="space60"
borderBottomWidth="borderWidth10"
borderBottomStyle="solid"
borderBottomColor="colorBorder"
>
{children}
</Box>
);
};
Card.Body = function CardBody({ children }) {
return <Box padding="space60">{children}</Box>;
};
Card.Footer = function CardFooter({ children }) {
return (
<Box
padding="space60"
borderTopWidth="borderWidth10"
borderTopStyle="solid"
borderTopColor="colorBorder"
>
{children}
</Box>
);
};
// Usage
<Card>
<Card.Header>Header content</Card.Header>
<Card.Body>Body content</Card.Body>
<Card.Footer>Footer content</Card.Footer>
</Card>
Render Props Pattern
function DataDisplay({ data, render }) {
return (
<Box padding="space60">
{render(data)}
</Box>
);
}
// Usage
<DataDisplay
data={{ name: 'John', role: 'Developer' }}
render={(data) => (
<Stack orientation="vertical" spacing="space30">
<Text>Name: {data.name}</Text>
<Text>Role: {data.role}</Text>
</Stack>
)}
/>
Next Steps
- Learn about Style Props available on Box
- Explore Design Tokens for styling values
- Review CustomizationProvider for theme overrides
- Check out Paste’s component source code for examples