Component Anatomy
Squeeze components are part of the @curology/ui-components-web package. While only React is currently supported, Web Components may be explored in the future. To this end, all code lives (and is exported) under a react sub-directory.
For instructions on cloning and running Squeeze locally, see Running Locally.
Source code
Component code lives in packages/ui-components-web/src/react.
Documentation
Documentation lives in apps/docs/docs/components, with navigation entries for each component in apps/docs/sidebars.js.
Component basics
When adding to or changing the components in this library, there are several basic concepts repeated throughout the code you should understand.
Extending HTML elements
Squeeze's components are designed to feel like native HTML elements, and to this end accept all the same props as their native counterparts. For example, the <Button /> component accepts all the same props as its <button> element, and the <TextInput /> component accepts all the same props as an <input> element. There are, of course, additional props added to each component to support Squeeze-specific functionality, but the goal is to make the components feel as native as possible.
Polymorphism
It's often useful to change which underlying HTML element a component renders. This polymorphism is a key principal of this library as it makes the components much more contextually flexible. Most Squeeze components expose an as prop that allows the underlying element to be changed. For example, while the <Button /> component is a <button> by default, it can be rendered as an <a> (<Button as="a" />) or <Link> (<Button as={Link} />) as well. See forwardRefWithAs.
Conditional classes
The components of Squeeze are highly configurable, which often means applying Tailwind classes conditionally based on given props. To encapsulate this conditional logic in a sane way, Squeeze leverages cva to build maps of classes that can be applied to a component based on its props. For example, the Button component has a buttonType prop that can be set to primary, secondary, or tertiary. Each of these types has a different set of classes that should be applied to the button. Rather than writing a bunch of if statements to handle this, we can use cva to create a map of classes that can be applied to the button based on its buttonType prop. See the Button component for an example.
Note: some older components may use a different approach to conditional classes using clsx, although we're in the process of migrating them.
Generic props, composable components
As mentioned above, one goal of Squeeze components is to make them feel like their native HTML counterparts. To this end, we should avoid a proliferation of props that muddy a component's API. Whenever possible, prefer composability over adding new props.
For example, the <Banner> component accepts an iconEnd prop. Instead of adding an onIconEndClicked prop to handle clicks on the icon, we can instead compose an icon with its own click handler, like <CloseIcon as="button" onClick={handler} />. This allows us to keep the <Banner> component's API clean while still allowing for the same functionality.
Jotai state management
When components need to hold state, they mostly use Jotai with our custom withAtomProvider[#withAtomProvider] HOC to create bounded contexts for sharing state between a component and its children. See the Navigation component for an example.
Note: some older components use React Context instead of Jotai, although we're in the process of migrating them.
Higher Order Components
Most components are wrapped in a few HOCs that allow for easy reuse of key functionality.
createComponent
This HOC handles setting the displayName on each component to make debugging easier.
forwardRefWithAs
This HOC creates a polymorphic component that can have its underlying HTML element altered. Any component wrapped in this function will receive an as prop that can be set to any HTML tag or React Component.
withAtomProvider
This HOC allows components to have scoped Jotai contexts that can be nested inside of each other. See the file for more specifics on why this is important, and why normal Jotai providers aren't enough.
Building block components
<MotionContainer />
Lazy loads framer-motion so that only the minimal amount of the Framer API is included at render time, with the rest being imported later. Results in a faster first paint.
Hooks
There are many custom hooks both used internally and exported by this library. See them all in src/react/helpers/hooks.
Theming
While most component design differences between themes can be handled by changing design tokens, Squeeze also has the ability to apply different functionality to a component based on the currently active brand. As noted above, we use cva to generate style variants based on props. We can extend this paradigm to include the currently active brand. For example, if we wanted a primary Agency button to have square corners, we'd need to add code to the component to support that:
const getButtonClasses = cva(
[
// ... some static classes
],
{
compoundVariants: [
{
brand: Brand.Agency,
buttonType: 'primary',
className: ['cur-rounded-none'],
},
],
},
);
export const Button = ({ buttonType }) => {
const brand = useBrand();
const className = buttonClasses({ brand, buttonType });
// ...
}
Scaffolding a new component
You can scaffold a new React component by running pnpm plop from the root of this project. This will create placeholder component code, tests, and docs.
Non-React code
Code that is not specific to the React components in this package lives outside of the src/react directory. This includes helper methods, icons, breakpoint definitions, etc.
Next-Specific code
Squeeze's React components are not tightly coupled to next.js, but there are some Next-specific exports from the library. See Next.js for more information.