├── .gitignore
├── README.md
├── part1
├── README.md
├── client
│ ├── public
│ │ ├── css
│ │ │ └── application.css
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ └── javascript
│ │ │ ├── application.js
│ │ │ ├── components.js
│ │ │ └── events.js
│ └── src
│ │ └── server.ts
└── webservers
│ ├── deno
│ ├── dynamic.ts
│ ├── hardcoded.ts
│ └── static.ts
│ ├── express
│ ├── package-lock.json
│ ├── package.json
│ └── src
│ │ ├── dynamic.js
│ │ ├── hardcoded.js
│ │ └── static.js
│ └── public
│ ├── index.html
│ ├── linked.html
│ └── styles.css
├── part2
└── README.md
└── part3
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .env
3 | node_modules
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UI Architecture
2 |
3 | ## Part One
4 |
5 | 1. Early Years
6 | 2. Requests
7 | 3. Managing State
8 | 4. Mutation
9 | 5. User Experience
10 | 6. Progressive Enhancement
11 | 7. Handling Events
12 | 8. Modifying The Document
13 | 9. Transport
14 | 10. The Platform
15 |
16 | ## Part Two
17 |
18 | 1. Complexity
19 | 2. Components
20 | 3. Managing State
21 | 4. Side Effects
22 | 5. Ecosystem
23 |
24 | ## Part Three
25 |
26 | 1. Abstraction
27 | 2. CSS
28 | 3. Tooling
29 | 4. Tailwind
30 | 5. CSS-in-JS
--------------------------------------------------------------------------------
/part1/README.md:
--------------------------------------------------------------------------------
1 | # UI Architecture - Part One
2 |
3 | ## The Platform
4 |
5 | - Requests
6 | - Managing State
7 | - Mutation
8 | - User Experience
9 | - Progressive Enhancement
10 | - Handling Events
11 | - Modifying The Document
12 | - Transport
13 |
14 | ## 1. Early Years
15 |
16 | Sharing information is intrinsic to human communities. As technology advances, we use those advances to improve our ability to communicate information.
17 |
18 | In the early 90s, [Tim Berners-Lee](https://en.wikipedia.org/wiki/Tim_Berners-Lee) saw an opportunity to combine computer networking and information communication. He released a [proposal](https://www.w3.org/History/1989/proposal.html) for a HyperCard-inspired approach to linking documents. Hypertext (HTML) and the Hypertext Transfer Protocol (HTTP) provide the basis for the modern internet. In 1991 he posted to [alt.hypertext]([https://www.w3.](https://www.w3.org/People/Berners-Lee/1991/08/art-6484.txt)org/People/Berners-Lee/1991/08/art-6484.txt) an invite to visit the [first website](http://info.cern.ch/hypertext/WWW/TheProject.html) and use software he developed based on his proposal.
19 |
20 | In 1994, Netscape Communications Corporation was the first company to try and capitalize on the World Wide Web. Their Netscape Navigator browser grew in popularity quickly; it was superior to their competitors. A documentary, [Project Code Rush](https://www.youtube.com/watch?v=4Q7FTjhvZ7Y), describes the end of Netscape during their effort to release its browser code as open-source software. It is worth watching.
21 |
22 | The World Wide Web spread into people's homes, and at this point, anyone with an internet connection was likely using Netscape Navigator to browse documents described as HTML. A specification for HTML or HTTP would be incomplete when Netscape added them to their browser.
23 |
24 | Setting up a server using the software provided by Tim Berners-Lee was a barrier to entry for most people if they wanted to post content on the WWW. GeoCities, a web company that started as Beverly Hills Internet, became a very popular web hosting provider.
25 |
26 | Users choose a neighbourhood based on the topic of their website. When they sign up, they receive a four-digit address in that neighbourhood. Someone might find a website about computer games using the [URL](https://en.wikipedia.org/wiki/URL) http://www.geocities.com/SiliconValley/4336/. Yahoo purchased GeoCities for $3.57 billion worth of stock in January 1999.
27 |
28 | > Demo [neocities](https://neocities.karl.sh/) available on [GitHub](https://github.com/jensen/neocities/).
29 |
30 | ## 2. Requests
31 |
32 | When we start a web server, it opens a port and listens for TCP connections. A basic web server only performs work when a user agent makes a request.
33 |
34 | > Example of a hard-coded web servers with [express](./webservers/express/src/hardcoded.js) and [Deno](./webservers/deno/hardcoded.ts).
35 |
36 | This type of web server requires a restart whenever the contents of the document change. We can alter our web server code to read files and serve them as documents when we find them.
37 |
38 | > Example of a static web servers with [express](./webservers/express/src/static.js) and [Deno](./webservers/deno/static.ts).
39 |
40 | After visiting a URL, the server returns a document, and the browser parses the document and makes further requests for linked resources. If a user clicks on a link or submits a form, then the browser makes a request to a server.
41 |
42 | > Example showing linked requests in the Network tab.
43 |
44 | Apache and NGINX are the most common production web servers available today. These servers do an excellent job of serving static files. Another option is deploying static assets to a CDN so that users download the files from servers closer to them. With these approaches available, Rails does not serve static files in production by default.
45 |
46 | ## 3. Managing State
47 |
48 | The ability to request documents and view the latest version on demand proves valuable. We can turn to dynamic web servers when we only need to provide more dynamic information. A server generates a dynamic web page by constructing the content when a user makes a request.
49 |
50 | > Example of a dynamic web servers with [express](./webservers/express/src/dynamic.js) and [Deno](./webservers/deno/dynamic.ts).
51 |
52 | The server can return relevant data when we provide search parameters as part of the URL. It can also create links to other paths within the site using search parameters that allow us to pass state as part of the URL.
53 |
54 | Lou Montulli invented the cookie as a Netscape employee in 1994. Before browser cookies, it was tough to store user state between requests. A server creates a cookie in response to a request from the browser. The browser holds the cookie and sends it with every following request to that site. The server can replace the cookie when values need to be updated.
55 |
56 | ## 4. Mutation
57 |
58 | We introduce most of the complexity in web development by allowing users to mutate data. The most popular sites of our time have ways for users to contribute content. Unknown to most, they submit a form with side effects by using the POST method with their content forming the body data of the request.
59 |
60 | The [HTML 2.0](https://www.w3.org/MarkUp/html-spec/html-spec_toc.html) specification contains the `
--------------------------------------------------------------------------------
/part1/webservers/public/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "Verdana", sans-serif;
3 | }
4 |
--------------------------------------------------------------------------------
/part2/README.md:
--------------------------------------------------------------------------------
1 | # UI Architecture - Part Two
2 |
3 | ## Modern Experiences
4 |
5 | - Components
6 | - Managing State
7 | - Side Effects
8 | - Ecosystem
9 |
10 | ## 1. Complexity
11 |
12 | Architecture is a term that describes the process of making decisions that allow us to deliver the feature we need today while maintaining a level of changeability that enables us to adapt to our changing environment. We can use architecture to manage the complexity of a project.
13 |
14 | Good user experience is a competitive advantage for a business. The details of what we declare a “good” experience vary, but as developers, we recognize that a commitment to creating a good user experience can cause the code we write to increase in complexity. We must deliver an experience that matches growing user expectations. Modern web applications use JavaScript to enhance the experience in many ways.
15 |
16 | - Smooth transitions when interacting with the document.
17 | - Retention of user state between transitions.
18 | - Multiple methods of user interaction to trigger transitions.
19 | - Updates automatically with multi-user editing.
20 |
21 | It is common to use a library or framework to build web applications. Hotwire with Rails, React, Vue and Angular are all examples. React frameworks like Next.js and Remix provide a fast initial load with server-rendered HTML. These tools primarily want to make it easier to build an experience that matches what the user expects.
22 |
23 | React is popular. The State of JS survey from 2021 has it listed at 80%, with Angular being the next most popular at 54%. Many job postings include React as an experience requirement and popular libraries we use with React.
24 |
25 | Understanding React application architecture helps us build software that we can maintain in the long term. Breaking an application into components does not automatically provide a clean architecture. The decisions around storing state, handling side-effects and styling are what allow us to reduce bugs and spend more time adding new features. These seven goals can help during the decision-making process.
26 |
27 | ### Think in interfaces
28 |
29 | Specifically the interface of each component. The interface of a component is defined by the props we pass and the elements that the component returns. When designing the interface of a component we must understand the purpose of the component, and create a minimal interface to satisfy the requirements.
30 |
31 | ### Minimize duplicate JSX
32 |
33 | Component-based user interfaces benefit from reuse, we can watch for repetition in our JSX and decide how to divide the application into components. When we design these components we can use composition patterns and ensure that our conditions are localized to reduce the duplication of JSX.
34 |
35 | ### Minimize duplicate state
36 |
37 | React allows us to store state in any component that we create. When we store the same state in more than one location we need to ensure that the state remains in sync. We can avoid this challenge by ensuring that all state has a single origin.
38 |
39 | ### Prefer handlers for side-effects
40 |
41 | Side effects should be triggered through event handlers primarily. Sometimes we need to use effects to synchronize our components with external systems.
42 |
43 | ### Identify root dependencies
44 |
45 | We can pass state from parent to child as props. When integrating the components in our application we will combine the state from various sources to create new types of data. A clear understanding of the path from the data we display back to its root sources helps debug issues.
46 |
47 | ### List all dependencies, and reduce them if possible
48 |
49 | The dependencies of useMemo, useCallback and useEffect are determined by the code that they contain. We need to include all dependencies in the dependency array to avoid bugs related to stale closure. It can be easier to list all dependencies when they are few.
50 |
51 | ### Use context to share things that don't change
52 |
53 | The React context API can reduce the number of props that we need to pass down through many layers of a component tree. If we aren’t careful, creating many context providers can result in a poor developer experience. We want to minimize our use of context, when we do use it, we should use it for values that do not change often. The provider should be located at the lowest point of the tree that allows access from all the required consumers.
54 |
55 | ## 2. Components
56 |
57 | Components are responsible for returning React elements. A static site will always return the same tree each time we render it. Dynamic sites will cause mutations to the DOM based on the state change of state within the application.
58 |
59 | We should define categories of components that are associated with base responsibilities. This has an impact on the structure of a project and helps us limit the workload for any one component.
60 |
61 | - Shared-use UI controls
62 | - Single-use UI controls
63 | - Page containers
64 | - Context providers
65 | - Layout containers
66 | - Pure props
67 |
68 | Within the definition of our component, we decide the styles we apply want to apply to elements. There are a lot of ways to style React components, in this example we are providing a class name to elements. The `cx` function is using a helper called `classnames` to conditionally apply styles.
69 |
70 | ```jsx
71 | const NavigationItem = (props) => {
72 | const Icon = props.icon;
73 |
74 | return (
75 |
80 |
81 |
82 |
83 |
84 | {props.label}
85 |
86 | );
87 | };
88 | ```
89 |
90 | We also choose which elements we display with conditional rendering. We use this pattern to show errors, modals, loading icons and empty states.
91 |
92 | ```jsx
93 | export default function EntryList(props) {
94 | const entries = useEntries({
95 | filter: {
96 | year: props.year,
97 | month: props.month,
98 | day: props.day,
99 | },
100 | });
101 |
102 | if (entries.length === 0) {
103 | return ;
104 | }
105 |
106 | return (
107 |
112 | );
113 | }
114 | ```
115 |
116 | Any feature that needs to synchronize our components with external systems can use effects. We can provide keyboard based input by registering the necessary event handlers.
117 |
118 | ```jsx
119 | useEffect(() => {
120 | const handleKeyPress = (event: KeyboardEvent) => {
121 | if (event.key === "ArrowLeft") {
122 | onPrevious();
123 | }
124 |
125 | if (event.key === "ArrowRight") {
126 | onNext();
127 | }
128 | };
129 |
130 | document.addEventListener("keyup", handleKeyPress);
131 |
132 | return () => {
133 | document.removeEventListener("keyup", handleKeyPress);
134 | };
135 | }, [onPrevious, onNext]);
136 | ```
137 |
138 | Often we will transform data from one type to another before we display it or pass it to a child component. This code exists as a custom hook to avoid having to repeat this code in each component that needs access to the same structure.
139 |
140 | ## 3. Managing State
141 |
142 | There is no single way to manage the state in a complex application. Depending on the purpose of the state, we might choose a different way to manage it. Our initial considerations determine if the state is required since we want only to store the minimal state necessary. If we require new state, we can categorize it to determine how to manage it.
143 |
144 | ### Server cache
145 |
146 | Persistent data mostly comes from a server. We must store the data we retrieve through an API within the application. We can store the data in a page container and then re-retrieve it each time the container is loaded. We can also store it higher in the tree, so it is available even when the component that requires it reloads.
147 |
148 | ### Local storage
149 |
150 | The browser can store data for a web application in the browser. The local storage API lets us set and get values using a key. Local storage is an external system; using an effect allows us to synchronize with React state.
151 |
152 | ### Session
153 |
154 | A successful login to a site can result in a response that sets a cookie in the browser. It is not common to attempt to read this session within the client application. Instead, we benefit from the automatic behaviour of sending the cookie back to the server with every subsequent request.
155 |
156 | ### Visual
157 |
158 | State that we use to apply conditional styles or conditionally render elements can live in. the closest common ancestor that needs it.
159 |
160 | ### Controlled input
161 |
162 | Input elements store state. When we need to synchronize the state of an input to a React component, we can use a controlled input pattern. We should store this state in the closest common ancestor of the components that need access. There are other form patterns that we can use to avoid controlled inputs.
163 |
164 | ### Input validation
165 |
166 | The validation of user input may result in the need to display an error message. Client-side validation is purely a user experience consideration. The server will need to validate inputs before performing a mutation.
167 |
168 | ### Async operation
169 |
170 | We can combine the state used to track a network request's loading, data and error state. The loading state is set to true when we start the operation. If the operation is successful, we set the data. If the operation results in an error, then we set the error state.
171 |
172 | ### Navigation
173 |
174 | The URL is one of the oldest locations to store state for web applications. Instead of the server parsing the URL for params, we can perform this same operation in our web client. We can conditionally render components based on the current location and use params to make our components display resource-specific data.
175 |
176 | ## 4. Side Effects
177 |
178 | React Hooks have been publicly available for over three years. Since React 16.8, avoiding bad advice on using the “useEffect” Hook has been challenging. Educators that intend to help developers learn how to use Hooks can often provide a minimal explanation that is simple but wrong.
179 |
180 | > 🚫 “Leave the dependency array empty if you only want to run the effect on mount.”
181 |
182 | This advice is a simple way to think about it, but what if there are dependencies? It is the contents of “useEffect” that define the dependencies. We cannot remove a dependency by leaving it out of our dependency array. Doing so creates stale closure defects that can be hard to debug. Ignoring the eslint error for the “exhaustive-deps” rule prevents any further reporting that may be valid. With React 18, all components will mount, unmount and mount again when we enable Strict Mode.
183 |
184 | Instead of taking the simple but wrong path, we can learn the techniques that allow us to include all our dependencies. If we want to avoid the infinite render loops, we need to understand referential stability.
185 |
186 | ```jsx
187 | function UserProfile() {
188 | const [user, setUser] = useState(null);
189 |
190 | const getUser = () =>
191 | fetch("/api/user/me")
192 | .then((response) => response.json())
193 | .then((user) => setUser(user));
194 |
195 | useEffect(() => {
196 | getUser();
197 | }, []); // getUser is missing from the array
198 |
199 | return user &&
{user.name}
;
200 | }
201 | ```
202 |
203 | A common technique for fetching data requires a `useEffect` with a call to fetch data and then set the data as React state when the response is successful. Calling a helper function that we declare in our component from a `useEffect` creates a dependency on an object that does not have a stable reference.
204 |
205 | If we decide that we want to show user information for any user with an`id` we can change the request to include a param. When the component mounts, it will load data for the user with the value defined by `props.id`. If the value of `props.id` changes; this code will not run to retrieve data for the user with the `id`.
206 |
207 | ```jsx
208 | function UserProfile(props) {
209 | const [user, setUser] = useState(null);
210 |
211 | const { id } = props;
212 |
213 | const getUser = () =>
214 | fetch(`/api/user/${id}`)
215 | .then((response) => response.json())
216 | .then((user) => setUser(user));
217 |
218 | useEffect(() => {
219 | getUser();
220 | }, []);
221 |
222 | return user &&
{user.name}
;
223 | }
224 | ```
225 |
226 | When we have a dependency, we include it in the array. To reduce our dependencies, we can move the helper function into the `useEffect` or remove the helper function wrapper altogether. We do not need to declare `setUser` as a dependency because it is guaranteed stable. If the value of `props.id` changes, this code will run to retrieve the data for the user with the `id`.
227 |
228 | ```jsx
229 | function UserProfile(props) {
230 | const [user, setUser] = useState(null);
231 |
232 | const { id } = props;
233 |
234 | useEffect(() => {
235 | fetch(`/api/user/${id}`)
236 | .then((response) => response.json())
237 | .then((user) => setUser(user));
238 | }, [id]);
239 |
240 | return user &&
{user.name}
;
241 | }
242 | ```
243 |
244 | The dependency array becomes complicated when we start passing objects to children as props. When a component React renders a component, the variables created during the calling of the function will not maintain the same reference from a previous render. A new object could have the same shape and data, but they will not be equal if we compare it to the last object by reference.
245 |
246 | ```jsx
247 | const { get, set } = useCache(key);
248 |
249 | useEffect(() => {
250 | let ignore = false;
251 |
252 | request().then((data) => {
253 | if (ignore === false) {
254 | set(data);
255 | }
256 | });
257 |
258 | return () => {
259 | ignore = true;
260 | };
261 | }, [request, set]);
262 | ```
263 |
264 | The `useCache` Hook creates the `set` function whenever a component calls it. When we create a dependency on a function, like `set`, we need to ensure that any closure `set` has is updated with the latest values. If the act of rendering causes us to recreate the `set` function without any of its root dependencies changing, then we can cause an infinite loop.
265 |
266 | We can use the `useCallback` Hook to ensure that we only create a new reference for the `set` function when any dependencies change.
267 |
268 | ```jsx
269 | const set = useCallback(
270 | (value) => setCache((cache) => ({ ...cache, [key]: value })),
271 | [key, setCache]
272 | );
273 | ```
274 |
275 | When a state mutation depends on the state's existing value, we can run into more common problems. When we use the reducer form of `setCache` we avoid creating a dependency on the existing `cache` state. This technique is one of the ways that we can reduce our dependencies. If we don't use the reducer, the `set` function has a new reference each time we update the cache.
276 |
277 | ```jsx
278 | const set = useCallback(
279 | (value) => setCache({ ...cache, [key]: value }),
280 | [key, cache, setCache]
281 | );
282 | ```
283 |
284 | This reference instability can cascade to child components, causing any effect that depends on `set` to run in a loop without further protection. In this next example, we only request if the data value is currently null. The dependency array includes all of the dependencies for this effect.
285 |
286 | ```jsx
287 | useEffect(() => {
288 | if (data !== null) return;
289 |
290 | let ignore = false;
291 |
292 | request().then((data) => {
293 | if (ignore === false) {
294 | set(data);
295 | }
296 | });
297 |
298 | return () => {
299 | ignore = true;
300 | };
301 | }, [request, data, set]);
302 | ```
303 |
304 | Instead of removing a dependency that belongs in the array, try one of the following approaches to reduce dependencies and control referential stability.
305 |
306 | - Move the function into the effect, or remove the wrapper.
307 | - Use the useCallback Hook with a full dependency list.
308 | - Use a reducer function with the set state actions.
309 | - Execute effects conditionally within the effect function.
310 | - Split up effects into multiple useEffect hooks.
311 |
312 | With the correct dependency list in place, we can ensure that we return a function from our effect, that allows for cleanup. We can use this to unsubscribe from events, clear timers or ignore the response of an HTTP request.
313 |
314 | ## 5. Ecosystem
315 |
316 | Many packages exist that provide abstractions for React applications. We can learn a lot from these packages, even if we choose to use a limited number of them.
317 |
318 | ### State Management
319 |
320 | [https://redux-toolkit.js.org/](https://redux-toolkit.js.org/)
321 |
322 | [https://github.com/pmndrs/zustand](https://zustand-demo.pmnd.rs/)
323 |
324 | [https://mobx.js.org/](https://mobx.js.org/README.html)
325 |
326 | [https://recoiljs.org/](https://recoiljs.org/)
327 |
328 | [https://jotai.org/](https://jotai.org/)
329 |
330 | [https://xstate.js.org/](https://xstate.js.org/)
331 |
332 | ### Routing
333 |
334 | [https://reactrouter.com/](https://reactrouter.com/)
335 |
336 | [https://react-location.tanstack.com/](https://react-location.tanstack.com/)
337 |
338 | ### Server Cache Management
339 |
340 | [https://react-query.tanstack.com/](https://react-query.tanstack.com/)
341 |
342 | [https://www.apollographql.com/docs/react/](https://www.apollographql.com/docs/react/)
343 |
344 | [https://relay.dev/](https://relay.dev/)
345 |
346 | [https://swr.vercel.app/](https://swr.vercel.app/)
347 |
348 | [https://formidable.com/open-source/urql/](https://formidable.com/open-source/urql/)
349 |
350 | ### Forms
351 |
352 | [https://formik.org/](https://formik.org/)
353 |
354 | [https://react-hook-form.com/](https://react-hook-form.com/)
355 |
356 | ### Frameworks
357 |
358 | [https://nextjs.org/](https://nextjs.org/)
359 |
360 | [https://remix.run/](https://remix.run/)
361 |
--------------------------------------------------------------------------------
/part3/README.md:
--------------------------------------------------------------------------------
1 | # UI Architecture - Part Three
2 |
3 | ## Style
4 |
5 | - Abstraction
6 | - CSS
7 | - Tooling
8 | - Tailwind
9 | - CSS-in-JS
10 |
11 |
12 | ## 1. Abstraction
13 |
14 | Building web interfaces is complicated. Challenges with layout and styling can be daunting when a project reaches a larger scale. These challenges have driven many in the web development community to build abstractions that ease the process. Various CSS frameworks, component libraries and toolchains provide a strong foundation for modern approaches to styling. On occasion, new CSS specifications include features first available from third-party providers.
15 |
16 | Our ability to share libraries full of components leads to an incredible increase in productivity. Since good design follows patterns, most libraries implement the same types of components. It is nice not to write all the code for a button with different variants or a complex table component, but this does not include all the benefits.
17 |
18 | A design system is what provides structure to a project. It takes less time to build when we already have the answer to most questions surrounding look and feel. The component libraries worth using are all based on a design system. The following are reasonable choices for a new project [Material UI](https://mui.com/), [Bootstrap](https://react-bootstrap.github.io/), [Polaris](https://polaris.shopify.com/components), [Primer](https://primer.style/react/), [Chakra UI](https://chakra-ui.com/), [Fluent UI](https://developer.microsoft.com/en-us/fluentui), [Blueprint](https://blueprintjs.com/), [Rebass](https://rebassjs.org/), [BaseWeb](https://baseweb.design/), [Garden](https://garden.zendesk.com/), [ADS](https://atlassian.design/), [Protocol](https://protocol.mozilla.org/), [Lightning](https://www.lightningdesignsystem.com/), [Cedar](https://rei.github.io/rei-cedar-docs/), [Carbon](https://carbondesignsystem.com/), [Elastic UI](https://elastic.github.io/eui/), [Theme UI](https://theme-ui.com/), and [Mantine](https://mantine.dev/).
19 |
20 | It is not typical to mix these libraries; a project should contain at most one of these dependencies. We can break this rule if a project is actively moving from one UI library to another. To choose a library, we can follow a process where we identify the components we need; once we have filtered the list to include libraries that contain the components we need, we determine which approach each one uses for styling.
21 |
22 | Sometimes it is enough to choose a library that looks good enough to ship as is, especially for internal projects where branding isn’t necessary. When we are building a project that requires a specific visual design, the most critical part of the UI library to understand is the mechanism we can use to change how it looks. If we design a project using an existing design system as a basis, it is easier to configure our theme to match our target look.
23 |
24 | UI libraries are expensive dependencies; they offer a lot of benefits when getting started but can present challenges when we are pushed outside of their constraints. In some cases, teams may decide to implement a custom library of components for their project. Using any approach found in popular UI libraries would provide a good foundation.
25 |
26 | Lately, utility-first CSS frameworks have become popular, with [Tailwind CSS](https://tailwindcss.com/) leading the category. These frameworks don’t provide components and instead re-present CSS properties as classes that can be composed using the `class` attribute of an HTML element. [Windi CSS](https://windicss.org/), [Tachyons](http://tachyons.io/), [Basscss](https://basscss.com/) and [Master CSS](https://css.master.co/) frameworks also come with a built-in design system that we can customize.
27 |
28 | One of the significant differences between a UI library and a CSS framework is the separation of concerns. A UI library tends to bundle how the component looks and how it behaves as a single consideration. CSS frameworks focus on providing tools to implement the visual design of components within an interface quickly, but they do not take any responsibility for functionality.
29 |
30 | In general, we can build components that do not have an opinion on how they look, the term used to categorize these types of components is Headless UI. Libraries like [Downshift](https://www.downshift-js.com/) and [TanStack Table](https://tanstack.com/table/v8) embrace this by providing accessible components we can style using any technique.
31 |
32 | [Headless UI](https://headlessui.com/), [Radix](https://www.radix-ui.com/), [React Aria](https://react-spectrum.adobe.com/react-aria/), [Reakit](https://reakit.io/) and [Ariakit](https://ariakit.org/) are all built with this goal in mind. We can compose CSS frameworks and Headless UI libraries to create accessible components that follow a design system. The Headless UI library pairs well with TailwindCSS, while Radix and [Stitches](https://stitches.dev/) provide suitable alternatives to fill the same role.
33 |
34 | These abstractions help us build interfaces faster, but they do not allow us to skip understanding the platform-supported features. Avoid buying site templates unless all technologies are well understood; they can cause more confusion than building on a proven design system.
35 |
36 | ## 2. CSS
37 |
38 | Building a design system for a large project using plain CSS is a significant undertaking. Unsurprisingly, smaller teams will reach for an existing solution to save development time. It is still important to understand how the platform works so that we can use the Chrome DevTools when something goes wrong.
39 |
40 | CSS allows us to alter how our document looks when it renders. We can break this responsibility into two sub-categories; Layout and Styling. Styling is more straightforward since it mainly involves altering the properties of our typography, including the size and colour. Originally the browser was designed to render documents, not applications.
41 |
42 | CSS has added numerous features since its inception to provide tools for layout. With the box model, the initial layout uses static positioning. We can make an element positioned using the values `relative` or `absolute`. Elements we position using `absolute` also fall out of the normal flow of content. We can use this to stack elements in our UI.
43 |
44 | Positioning elements based on screen coordinates isn’t very responsive, so we must be cautious when using this approach. We often use Flexbox, Grid, or a combination of the two when addressing our layout. We should lock our spacing values to a scale and apply padding or margins appropriately.
45 |
46 | A colour palette for a project is usually quite limited; it is a good practice to declare the colour values as CSS variables using semantic names. We can use Lint to ensure that all rules reference variables instead of hard-coded colour values.
47 |
48 | ## 3. Tooling
49 |
50 | Sass has been around for fifteen years; it provides numerous benefits to those who write large-scale CSS systems. Over time the CSS specification has evolved and now includes features previously only available with third-party tooling. As the platform grows, we should change our approach if it serves us well. For example, now that they are available when we are targeting modern browsers, we can use built-in CSS variables.
51 |
52 | The introduction of tooling to the CSS development process allows for incredible flexibility in how we author our CSS. As UI developers, we face challenges with organizing our system due to common issues such as name clashing, specificity overruling and browser compatibility. Tools like PostCSS help us by providing necessary features.
53 |
54 | We can reduce the complexity of CSS with features like automatic property prefixing, scoping class names and linting. We configure our tools to allow for non-standard features like nesting.
55 |
56 | ## 4. Tailwind
57 |
58 | Before CSS, it was possible to style text using the `` element with `color` or `size` attributes. When we embed our style with our structure, we seek ways to separate it as we scale. We don’t often recommend inline styles or embedded style tags for anything other than testing stuff out, even though it is pretty convenient to co-locate styles with our HTML.
59 |
60 | Advocates base their argument for using Tailwind CSS on the idea that naming things is hard. If we don’t have to come up with class names, we can quickly iterate on our designs. Naming is hard, but it is not the problem we avoid when using Tailwind CSS. Eventually, we will have to name something; avoiding it is not a solution.
61 |
62 | We can test Tailwind's highly configurable design system on their [Playground](https://play.tailwindcss.com/). We fill our markup with utility class names without tools like React to abstract components into smaller pieces. As our lists grow in length, we can benefit by using an auto-sorting prettier rule to ensure our class names have some level of organization.
63 |
64 | Tailwind Labs offers paid component templates called [Tailwind UI](https://tailwindui.com/) which can reduce the time launching a typical site. It has also inspired complete open source UI libraries like [daisyUI](https://daisyui.com/) and CSS-in-JS runtimes like [Twind](https://twind.dev/).
65 |
66 | ## 5. CSS-in-JS
67 |
68 | CSS-in-JS libraries provide the basis for most of the popular UI component libraries. If we use a UI component library that depends on Emotion, it is probably a good idea to use Emotion for our custom components. When we do this, we gain several benefits.
69 |
70 | The most obvious difference is that we can write our CSS directly in our component source files. These tools assign unique CSS class names to avoid a naming collision, and vendor prefixes are added to the resulting output to improve compatibility. Other benefits include good compatibility with SSR and theming support.
71 |
72 | When choosing a library, we should consider the style definition syntax, the style application syntax and the output format. The output format is usually a separate `.css` file or a runtime that creates one or more `