Introduction
This is part five of our series on building a complete design system from scratch. In the previous tutorial created the Badge
component. In this short tutorial we will create 2 components Spinner and Icon which we will use in the future for the Alert
& Button
components. I would encourage you to play around with the deployed storybook. All the code for this series is available on GitHub.
Step One: Spinner Component
We want to achieve the following for the Spinner
component -
<Spinner thickness="3px" speed="1s" size="sm" />
Under atoms
create a new folder feedback
and under feedback
create a new folder called spinner
. Under atoms/feedback/spinner
create the spinner.scss
file and paste the following -
@keyframes spinner-animation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.spinner {
--thickness: 2px;
--speed: 0.4s;
display: inline-block;
color: currentColor;
border-color: currentColor;
border-style: solid;
border-radius: 9999px;
border-width: var(--thickness);
border-bottom-color: transparent;
border-left-color: transparent;
width: 1em;
height: 1em;
animation: spinner-animation var(--speed) linear infinite;
&.xs {
width: 1.5rem;
height: 1.5rem;
}
&.sm {
width: 2rem;
height: 2rem;
}
&.md {
width: 2.5rem;
height: 2.5rem;
}
&.lg {
width: 3rem;
height: 3rem;
}
&.xl {
width: 3.5rem;
height: 3.5rem;
}
}
Notice for the thickness and speed we are using custom css properties, these will enable us to pass dynamic values at runtime.
Now under atoms/feedback/spinner
folder create index.tsx
file -
import * as React from "react";
import { cva, VariantProps } from "class-variance-authority";
import "./spinner.scss";
const spinner = cva(["spinner"], {
variants: {
size: {
xs: "xs",
sm: "sm",
md: "md",
lg: "lg",
xl: "xl",
},
},
});
export type SpinnerProps = VariantProps<typeof spinner> &
React.ComponentProps<"span"> & {
thickness?: string;
speed?: string;
};
export function Spinner(props: SpinnerProps) {
const { thickness, speed, size, style, className, ...delegated } = props;
const stylesWithVars = {
"--thickness": thickness,
"--speed": speed,
...style,
} as React.CSSProperties;
return (
<span
style={stylesWithVars}
className={spinner({ size, className })}
{...delegated}
/>
);
}
Take a note, that we are using the style
prop to set the thickness and speed
custom css properties dynamically, to pass in dynamic values at runtime we have to use custom css properties. And with that our Spinner
component is ready, you can create a story for it.
Finally, under atoms/feedback
create a new file index.ts
-
export * from "./spinner";
Step Two: Icon Component
Under atoms
create a new folder icons
under atoms/icons
folder create five files namely icon.scss
, icon.tsx
, icons.tsx
, create-icon.tsx
and index.ts
.
Under atoms/icons/icon.scss
paste the following -
.icon {
display: inline-block;
width: 1em;
height: 1em;
line-height: 1em;
vertical-align: middle;
}
Under atoms/icons/icon.tsx
paste the following -
import * as React from "react";
import { cva } from "class-variance-authority";
import "./icon.scss";
const fallbackIcon = {
path: (
<g stroke="currentColor" strokeWidth="1.5">
<path
strokeLinecap="round"
fill="none"
d="M9,9a3,3,0,1,1,4,2.829,1.5,1.5,0,0,0-1,1.415V14.25"
/>
<path
fill="currentColor"
strokeLinecap="round"
d="M12,17.25a.375.375,0,1,0,.375.375A.375.375,0,0,0,12,17.25h0"
/>
<circle fill="none" strokeMiterlimit="10" cx="12" cy="12" r="11.25" />
</g>
),
viewBox: "0 0 24 24",
};
const icon = cva(["icon"]);
export interface IconProps extends React.ComponentProps<"svg"> {}
export const Icon = (props: IconProps) => {
const {
viewBox = fallbackIcon.viewBox,
color = "currentColor",
focusable = false,
children,
className,
...delegated
} = props;
const path = (children ?? fallbackIcon.path) as React.ReactNode;
return (
<svg
className={icon({ className })}
color={color}
viewBox={viewBox}
focusable={focusable}
{...delegated}
>
{path}
</svg>
);
};
Under atoms/icons/create-icon.tsx
paste the following -
import * as React from "react";
import { Icon, IconProps } from "./icon";
interface CreateIconOptions {
viewBox?: string;
path?: React.ReactElement | React.ReactElement[];
d?: string;
defaultProps?: IconProps;
}
export function createIcon(options: CreateIconOptions) {
const {
viewBox = "0 0 24 24",
d: pathDefinition,
path,
defaultProps = {},
} = options;
const Component = (props: IconProps) => (
<Icon viewBox={viewBox} {...defaultProps} {...props}>
{path ?? <path fill="currentColor" d={pathDefinition} />}
</Icon>
);
return Component;
}
Now we will use the Icon
component and createIcon
function to make some new Icons, under atoms/feedback/icons.tsx
-
import * as React from "react";
import { createIcon } from "./create-icon";
import { Icon, IconProps } from "./icon";
export const CheckIcon = createIcon({
d: "M12,0A12,12,0,1,0,24,12,12.014,12.014,0,0,0,12,0Zm6.927,8.2-6.845,9.289a1.011,1.011,0,0,1-1.43.188L5.764,13.769a1,1,0,1,1,1.25-1.562l4.076,3.261,6.227-8.451A1,1,0,1,1,18.927,8.2Z",
});
export const InfoIcon = createIcon({
d: "M12,0A12,12,0,1,0,24,12,12.013,12.013,0,0,0,12,0Zm.25,5a1.5,1.5,0,1,1-1.5,1.5A1.5,1.5,0,0,1,12.25,5ZM14.5,18.5h-4a1,1,0,0,1,0-2h.75a.25.25,0,0,0,.25-.25v-4.5a.25.25,0,0,0-.25-.25H10.5a1,1,0,0,1,0-2h1a2,2,0,0,1,2,2v4.75a.25.25,0,0,0,.25.25h.75a1,1,0,1,1,0,2Z",
});
export const WarningIcon = createIcon({
d: "M11.983,0a12.206,12.206,0,0,0-8.51,3.653A11.8,11.8,0,0,0,0,12.207,11.779,11.779,0,0,0,11.8,24h.214A12.111,12.111,0,0,0,24,11.791h0A11.766,11.766,0,0,0,11.983,0ZM10.5,16.542a1.476,1.476,0,0,1,1.449-1.53h.027a1.527,1.527,0,0,1,1.523,1.47,1.475,1.475,0,0,1-1.449,1.53h-.027A1.529,1.529,0,0,1,10.5,16.542ZM11,12.5v-6a1,1,0,0,1,2,0v6a1,1,0,1,1-2,0Z",
});
export const WarningTwoIcon = createIcon({
d: "M23.119,20,13.772,2.15h0a2,2,0,0,0-3.543,0L.881,20a2,2,0,0,0,1.772,2.928H21.347A2,2,0,0,0,23.119,20ZM11,8.423a1,1,0,0,1,2,0v6a1,1,0,1,1-2,0Zm1.05,11.51h-.028a1.528,1.528,0,0,1-1.522-1.47,1.476,1.476,0,0,1,1.448-1.53h.028A1.527,1.527,0,0,1,13.5,18.4,1.475,1.475,0,0,1,12.05,19.933Z",
});
export const CloseIcon = createIcon({
d: "M.439,21.44a1.5,1.5,0,0,0,2.122,2.121L11.823,14.3a.25.25,0,0,1,.354,0l9.262,9.263a1.5,1.5,0,1,0,2.122-2.121L14.3,12.177a.25.25,0,0,1,0-.354l9.263-9.262A1.5,1.5,0,0,0,21.439.44L12.177,9.7a.25.25,0,0,1-.354,0L2.561.44A1.5,1.5,0,0,0,.439,2.561L9.7,11.823a.25.25,0,0,1,0,.354Z",
});
export const SearchIcon = createIcon({
d: "M23.384,21.619,16.855,15.09a9.284,9.284,0,1,0-1.768,1.768l6.529,6.529a1.266,1.266,0,0,0,1.768,0A1.251,1.251,0,0,0,23.384,21.619ZM2.75,9.5a6.75,6.75,0,1,1,6.75,6.75A6.758,6.758,0,0,1,2.75,9.5Z",
});
export const Search2Icon = createIcon({
d: "M23.414,20.591l-4.645-4.645a10.256,10.256,0,1,0-2.828,2.829l4.645,4.644a2.025,2.025,0,0,0,2.828,0A2,2,0,0,0,23.414,20.591ZM10.25,3.005A7.25,7.25,0,1,1,3,10.255,7.258,7.258,0,0,1,10.25,3.005Z",
});
export const PhoneIcon = createIcon({
d: "M2.20731,0.0127209 C2.1105,-0.0066419 1.99432,-0.00664663 1.91687,0.032079 C0.871279,0.438698 0.212942,1.92964 0.0580392,2.95587 C-0.426031,6.28627 2.20731,9.17133 4.62766,11.0689 C6.77694,12.7534 10.9012,15.5223 13.3409,12.8503 C13.6507,12.5211 14.0186,12.037 13.9993,11.553 C13.9412,10.7397 13.186,10.1588 12.6051,9.71349 C12.1598,9.38432 11.2304,8.47427 10.6495,8.49363 C10.1267,8.51299 9.79754,9.05515 9.46837,9.38432 L8.88748,9.96521 C8.79067,10.062 7.55145,9.24878 7.41591,9.15197 C6.91248,8.8228 6.4284,8.45491 6.00242,8.04829 C5.57644,7.64167 5.18919,7.19632 4.86002,6.73161 C4.7632,6.59607 3.96933,5.41495 4.04678,5.31813 C4.04678,5.31813 4.72448,4.58234 4.91811,4.2919 C5.32473,3.67229 5.63453,3.18822 5.16982,2.45243 C4.99556,2.18135 4.78257,1.96836 4.55021,1.73601 C4.14359,1.34875 3.73698,0.942131 3.27227,0.612963 C3.02055,0.419335 2.59457,0.0708094 2.20731,0.0127209 Z",
viewBox: "0 0 14 14",
});
export const EmailIcon = createIcon({
path: (
<g fill="currentColor">
<path d="M11.114,14.556a1.252,1.252,0,0,0,1.768,0L22.568,4.87a.5.5,0,0,0-.281-.849A1.966,1.966,0,0,0,22,4H2a1.966,1.966,0,0,0-.289.021.5.5,0,0,0-.281.849Z" />
<path d="M23.888,5.832a.182.182,0,0,0-.2.039l-6.2,6.2a.251.251,0,0,0,0,.354l5.043,5.043a.75.75,0,1,1-1.06,1.061l-5.043-5.043a.25.25,0,0,0-.354,0l-2.129,2.129a2.75,2.75,0,0,1-3.888,0L7.926,13.488a.251.251,0,0,0-.354,0L2.529,18.531a.75.75,0,0,1-1.06-1.061l5.043-5.043a.251.251,0,0,0,0-.354l-6.2-6.2a.18.18,0,0,0-.2-.039A.182.182,0,0,0,0,6V18a2,2,0,0,0,2,2H22a2,2,0,0,0,2-2V6A.181.181,0,0,0,23.888,5.832Z" />
</g>
),
});
export const ArrowForwardIcon = createIcon({
d: "M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z",
});
export const AvatarFallback: React.FC<IconProps> = (props) => {
return (
<Icon
viewBox="0 0 128 128"
color="#fff"
width="100%"
height="100%"
{...props}
>
<path
fill="currentColor"
d="M103,102.1388 C93.094,111.92 79.3504,118 64.1638,118 C48.8056,118 34.9294,111.768 25,101.7892 L25,95.2 C25,86.8096 31.981,80 40.6,80 L87.4,80 C96.019,80 103,86.8096 103,95.2 L103,102.1388 Z"
/>
<path
fill="currentColor"
d="M63.9961647,24 C51.2938136,24 41,34.2938136 41,46.9961647 C41,59.7061864 51.2938136,70 63.9961647,70 C76.6985159,70 87,59.7061864 87,46.9961647 C87,34.2938136 76.6985159,24 63.9961647,24"
/>
</Icon>
);
};
Finally under atoms/icons/index.ts
paste the following -
export * from "./icon";
export * from "./create-icon";
export * from "./icons";
Conclusion
In this tutorial we created the Spinner
& Icon
components, we will use these in the next tutorials. All the code for this tutorial can be found here. In the next tutorial we will create a theme able Alert
component. Until next time PEACE.
Top comments (0)