From 9b9c4e326bd7b188237157ae350abcba0563dd4a Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Mon, 8 Dec 2025 21:00:52 -0500 Subject: [PATCH 01/10] Add stack utility --- src/core/utilities.css | 12 ++++ src/stories/Utilities/Flex/Flex.mdx | 4 +- src/stories/Utilities/Stack/Stack.js | 13 ++++ src/stories/Utilities/Stack/Stack.mdx | 71 ++++++++++++++++++++ src/stories/Utilities/Stack/Stack.stories.js | 64 ++++++++++++++++++ src/stories/helpers/utils.js | 2 +- 6 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 src/stories/Utilities/Stack/Stack.js create mode 100644 src/stories/Utilities/Stack/Stack.mdx create mode 100644 src/stories/Utilities/Stack/Stack.stories.js diff --git a/src/core/utilities.css b/src/core/utilities.css index be98ce9a..bffac58b 100644 --- a/src/core/utilities.css +++ b/src/core/utilities.css @@ -417,3 +417,15 @@ clip: rect(1px, 1px, 1px, 1px); inline-size: 1px; } + +/* Advanced Layout Utilities */ + +/* Equivalent to .flex.flex-col.gap-md */ +.stack { + display: flex; + flex-direction: column; + + &:not([class*='gap-']) { + gap: var(--op-space-medium); + } +} diff --git a/src/stories/Utilities/Flex/Flex.mdx b/src/stories/Utilities/Flex/Flex.mdx index 0b6a4fc2..41055f46 100644 --- a/src/stories/Utilities/Flex/Flex.mdx +++ b/src/stories/Utilities/Flex/Flex.mdx @@ -107,9 +107,7 @@ A normal `div` without the flex utility `.items-start` places each flex item at the start of the cross axis. - - - + ## Align Items Center diff --git a/src/stories/Utilities/Stack/Stack.js b/src/stories/Utilities/Stack/Stack.js new file mode 100644 index 00000000..a8dafee5 --- /dev/null +++ b/src/stories/Utilities/Stack/Stack.js @@ -0,0 +1,13 @@ +import { createChildren } from '../../helpers/utils' + +export const createStack = ({ stack = true, alignItems = '', gap = '' }) => { + const wrapper = document.createElement('div') + + wrapper.className = [stack ? 'stack' : '', alignItems ? `items-${alignItems}` : '', gap ? `gap-${gap}` : ''] + .filter(Boolean) + .join(' ') + + createChildren(wrapper, 5) + + return wrapper +} diff --git a/src/stories/Utilities/Stack/Stack.mdx b/src/stories/Utilities/Stack/Stack.mdx new file mode 100644 index 00000000..88367d5c --- /dev/null +++ b/src/stories/Utilities/Stack/Stack.mdx @@ -0,0 +1,71 @@ +import { Meta, Story, Canvas, Controls } from '@storybook/addon-docs/blocks' +import * as StackStories from './Stack.stories' +import { createSourceCodeLink } from '../../helpers/sourceCodeLink.js' + + + +# Stack + +
+ +While flex utilities are great for moving quickly or for simple layouts, they can easily become +cumbersome to manage and tend to turn into "Flex Spaghetti". In some of these cases, reaching +for named components may make sense. However, there are times when a full component is more +restrictive than you need and hurts productivity. Sometimes you need just a bit more structure +to your layout, but not a component. Stacks provide a simple way to create readable, +flexible layouts without the overhead of a full component. + +The stack utility provides a simple way to create a vertical stack of spaced items. It +works with all of the gap and other flex utilities. It is effectively equivalent to using +`.flex .flex-col .gap-md` together. + +## Playground + + + + +## Without + +A normal `div` without the stack utility + + + +## Stack Property + +`.stack` Creates a flex stack. + + + +## Align Items Stretch + +`.items-stretch` Stretches each item across the cross axis. This is the default alignment and doesn't need to be applied unless you are overriding something set differently. + + + +## Align Items Start + +`.items-start` places each item at the start of the cross axis. + + + +## Align Items Center + +`.items-center` places each item at the center of the cross axis. + + + +## Align Items End + +`.items-end` places each item at the end of the cross axis. + + + +## Align Items Baseline + +`.items-baseline` places each item at the text baseline of the cross axis. + + diff --git a/src/stories/Utilities/Stack/Stack.stories.js b/src/stories/Utilities/Stack/Stack.stories.js new file mode 100644 index 00000000..53aaa95e --- /dev/null +++ b/src/stories/Utilities/Stack/Stack.stories.js @@ -0,0 +1,64 @@ +import { createStack } from './Stack.js' + +export default { + title: 'Utilities/Stack', + render: ({ stack, ...args }) => { + return createStack({ stack, ...args }) + }, + argTypes: { + stack: { control: 'boolean' }, + alignItems: { + control: { type: 'select' }, + options: ['stretch', 'start', 'center', 'end', 'baseline'], + }, + gap: { + control: { type: 'select' }, + options: ['xxs', 'xs', 'sm', 'md', 'lg', 'xl'], + }, + }, + parameters: { + layout: 'fullscreen', + }, +} + +export const With = { + args: { + stack: true, + }, +} + +export const Without = { + args: { + stack: false, + }, +} + +export const AlignStretch = { + args: { + alignItems: 'stretch', + }, +} + +export const AlignStart = { + args: { + alignItems: 'start', + }, +} + +export const AlignCenter = { + args: { + alignItems: 'center', + }, +} + +export const AlignEnd = { + args: { + alignItems: 'end', + }, +} + +export const AlignBaseline = { + args: { + alignItems: 'baseline', + }, +} diff --git a/src/stories/helpers/utils.js b/src/stories/helpers/utils.js index b9b318bf..bf6702ee 100644 --- a/src/stories/helpers/utils.js +++ b/src/stories/helpers/utils.js @@ -1,7 +1,7 @@ export const createChildren = (element, count) => { Array.from(Array(count)).forEach((_, _i) => { const box = document.createElement('div') - box.style.width = 'var(--op-space-x-large)' + box.style.minWidth = 'var(--op-space-x-large)' box.style.minHeight = 'var(--op-space-x-large)' box.style.backgroundColor = 'var(--op-color-primary-original)' element.appendChild(box) From 520d415f2ab66fb5e069f620d3c53e44c0046a61 Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Mon, 8 Dec 2025 21:25:53 -0500 Subject: [PATCH 02/10] Add cluster utility --- src/core/utilities.css | 14 ++++ src/stories/Utilities/Cluster/Cluster.js | 14 ++++ src/stories/Utilities/Cluster/Cluster.mdx | 71 +++++++++++++++++++ .../Utilities/Cluster/Cluster.stories.js | 64 +++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 src/stories/Utilities/Cluster/Cluster.js create mode 100644 src/stories/Utilities/Cluster/Cluster.mdx create mode 100644 src/stories/Utilities/Cluster/Cluster.stories.js diff --git a/src/core/utilities.css b/src/core/utilities.css index bffac58b..5f1254c6 100644 --- a/src/core/utilities.css +++ b/src/core/utilities.css @@ -429,3 +429,17 @@ gap: var(--op-space-medium); } } + +/* Equivalent to .flex.flex-wrap.items-center.gap-md */ +.cluster { + display: flex; + flex-wrap: wrap; + + &:not([class*='items-']) { + align-items: center; + } + + &:not([class*='gap-']) { + gap: var(--op-space-medium); + } +} diff --git a/src/stories/Utilities/Cluster/Cluster.js b/src/stories/Utilities/Cluster/Cluster.js new file mode 100644 index 00000000..7df063b9 --- /dev/null +++ b/src/stories/Utilities/Cluster/Cluster.js @@ -0,0 +1,14 @@ +import { createChildren } from '../../helpers/utils' + +export const createCluster = ({ cluster = true, alignItems = '', gap = '' }) => { + const wrapper = document.createElement('div') + wrapper.style.height = '15rem' + + wrapper.className = [cluster ? 'cluster' : '', alignItems ? `items-${alignItems}` : '', gap ? `gap-${gap}` : ''] + .filter(Boolean) + .join(' ') + + createChildren(wrapper, 35) + + return wrapper +} diff --git a/src/stories/Utilities/Cluster/Cluster.mdx b/src/stories/Utilities/Cluster/Cluster.mdx new file mode 100644 index 00000000..028281ce --- /dev/null +++ b/src/stories/Utilities/Cluster/Cluster.mdx @@ -0,0 +1,71 @@ +import { Meta, Story, Canvas, Controls } from '@storybook/addon-docs/blocks' +import * as ClusterStories from './Cluster.stories' +import { createSourceCodeLink } from '../../helpers/sourceCodeLink.js' + + + +# Cluster + +
+ +While flex utilities are great for moving quickly or for simple layouts, they can easily become +cumbersome to manage and tend to turn into "Flex Spaghetti". In some of these cases, reaching +for named components may make sense. However, there are times when a full component is more +restrictive than you need and hurts productivity. Sometimes you need just a bit more structure +to your layout, but not a component. Stacks provide a simple way to create readable, +flexible layouts without the overhead of a full component. + +The cluster utility provides a simple way to create a wrapping, group of spaced items. It +works with all of the gap and other flex utilities. It is effectively equivalent to using +`.flex .flex-wrap .items-center .gap-md` together. + +## Playground + + + + +## Without + +A normal `div` without the cluster utility + + + +## Cluster Property + +`.cluster` Creates a flex cluster. + + + +## Align Items Stretch + +`.items-stretch` Stretches each item across the cross axis. This is the default alignment and doesn't need to be applied unless you are overriding something set differently. + + + +## Align Items Start + +`.items-start` places each item at the start of the cross axis. + + + +## Align Items Center + +`.items-center` places each item at the center of the cross axis. + + + +## Align Items End + +`.items-end` places each item at the end of the cross axis. + + + +## Align Items Baseline + +`.items-baseline` places each item at the text baseline of the cross axis. + + diff --git a/src/stories/Utilities/Cluster/Cluster.stories.js b/src/stories/Utilities/Cluster/Cluster.stories.js new file mode 100644 index 00000000..dbba4f28 --- /dev/null +++ b/src/stories/Utilities/Cluster/Cluster.stories.js @@ -0,0 +1,64 @@ +import { createCluster } from './Cluster.js' + +export default { + title: 'Utilities/Cluster', + render: ({ cluster, ...args }) => { + return createCluster({ cluster, ...args }) + }, + argTypes: { + cluster: { control: 'boolean' }, + alignItems: { + control: { type: 'select' }, + options: ['stretch', 'start', 'center', 'end', 'baseline'], + }, + gap: { + control: { type: 'select' }, + options: ['xxs', 'xs', 'sm', 'md', 'lg', 'xl'], + }, + }, + parameters: { + layout: 'fullscreen', + }, +} + +export const With = { + args: { + cluster: true, + }, +} + +export const Without = { + args: { + cluster: false, + }, +} + +export const AlignStretch = { + args: { + alignItems: 'stretch', + }, +} + +export const AlignStart = { + args: { + alignItems: 'start', + }, +} + +export const AlignCenter = { + args: { + alignItems: 'center', + }, +} + +export const AlignEnd = { + args: { + alignItems: 'end', + }, +} + +export const AlignBaseline = { + args: { + alignItems: 'baseline', + }, +} From 4695e09d6c5df5ea4344eb6672803bee43ed9d74 Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Mon, 8 Dec 2025 21:57:45 -0500 Subject: [PATCH 03/10] Add split utility --- src/core/utilities.css | 15 +++++ src/stories/Utilities/Split/Split.js | 14 ++++ src/stories/Utilities/Split/Split.mdx | 70 ++++++++++++++++++++ src/stories/Utilities/Split/Split.stories.js | 64 ++++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 src/stories/Utilities/Split/Split.js create mode 100644 src/stories/Utilities/Split/Split.mdx create mode 100644 src/stories/Utilities/Split/Split.stories.js diff --git a/src/core/utilities.css b/src/core/utilities.css index 5f1254c6..809eb985 100644 --- a/src/core/utilities.css +++ b/src/core/utilities.css @@ -443,3 +443,18 @@ gap: var(--op-space-medium); } } + +/* Equivalent to .flex.flex-wrap.items-center.justify-between.gap-md */ +.split { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + + &:not([class*='items-']) { + align-items: center; + } + + &:not([class*='gap-']) { + gap: var(--op-space-medium); + } +} diff --git a/src/stories/Utilities/Split/Split.js b/src/stories/Utilities/Split/Split.js new file mode 100644 index 00000000..64875565 --- /dev/null +++ b/src/stories/Utilities/Split/Split.js @@ -0,0 +1,14 @@ +import { createChildren } from '../../helpers/utils' + +export const createSplit = ({ split = true, alignItems = '', gap = '' }) => { + const wrapper = document.createElement('div') + wrapper.style.height = '10rem' + + wrapper.className = [split ? 'split' : '', alignItems ? `items-${alignItems}` : '', gap ? `gap-${gap}` : ''] + .filter(Boolean) + .join(' ') + + createChildren(wrapper, 2) + + return wrapper +} diff --git a/src/stories/Utilities/Split/Split.mdx b/src/stories/Utilities/Split/Split.mdx new file mode 100644 index 00000000..62b83f81 --- /dev/null +++ b/src/stories/Utilities/Split/Split.mdx @@ -0,0 +1,70 @@ +import { Meta, Story, Canvas, Controls } from '@storybook/addon-docs/blocks' +import * as SplitStories from './Split.stories' +import { createSourceCodeLink } from '../../helpers/sourceCodeLink.js' + + + +# Split + +
+ +While flex utilities are great for moving quickly or for simple layouts, they can easily become +cumbersome to manage and tend to turn into "Flex Spaghetti". In some of these cases, reaching +for named components may make sense. However, there are times when a full component is more +restrictive than you need and hurts productivity. Sometimes you need just a bit more structure +to your layout, but not a component. Stacks provide a simple way to create readable, +flexible layouts without the overhead of a full component. + +The split utility provides a simple way to push two items apart from each other. +It is effectively equivalent to using `.flex .flex-wrap .items-center .justify-between .gap-md` together. + +## Playground + + + + +## Without + +A normal `div` without the cluster utility + + + +## Cluster Property + +`.cluster` Creates a flex cluster. + + + +## Align Items Stretch + +`.items-stretch` Stretches each item across the cross axis. This is the default alignment and doesn't need to be applied unless you are overriding something set differently. + + + +## Align Items Start + +`.items-start` places each item at the start of the cross axis. + + + +## Align Items Center + +`.items-center` places each item at the center of the cross axis. + + + +## Align Items End + +`.items-end` places each item at the end of the cross axis. + + + +## Align Items Baseline + +`.items-baseline` places each item at the text baseline of the cross axis. + + diff --git a/src/stories/Utilities/Split/Split.stories.js b/src/stories/Utilities/Split/Split.stories.js new file mode 100644 index 00000000..fb90f668 --- /dev/null +++ b/src/stories/Utilities/Split/Split.stories.js @@ -0,0 +1,64 @@ +import { createSplit } from './Split.js' + +export default { + title: 'Utilities/Split', + render: ({ split, ...args }) => { + return createSplit({ split, ...args }) + }, + argTypes: { + split: { control: 'boolean' }, + alignItems: { + control: { type: 'select' }, + options: ['stretch', 'start', 'center', 'end', 'baseline'], + }, + gap: { + control: { type: 'select' }, + options: ['xxs', 'xs', 'sm', 'md', 'lg', 'xl'], + }, + }, + parameters: { + layout: 'fullscreen', + }, +} + +export const With = { + args: { + split: true, + }, +} + +export const Without = { + args: { + split: false, + }, +} + +export const AlignStretch = { + args: { + alignItems: 'stretch', + }, +} + +export const AlignStart = { + args: { + alignItems: 'start', + }, +} + +export const AlignCenter = { + args: { + alignItems: 'center', + }, +} + +export const AlignEnd = { + args: { + alignItems: 'end', + }, +} + +export const AlignBaseline = { + args: { + alignItems: 'baseline', + }, +} From 19fb45787e1ce63ccf36ccbecd23f6c81076afc8 Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Mon, 8 Dec 2025 22:47:14 -0500 Subject: [PATCH 04/10] Add utility layout example --- src/stories/Recipes/Layout/Layout.js | 97 ++++++++++++++++++++ src/stories/Recipes/Layout/Layout.mdx | 6 ++ src/stories/Recipes/Layout/Layout.stories.js | 8 +- 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/src/stories/Recipes/Layout/Layout.js b/src/stories/Recipes/Layout/Layout.js index c5ad4b1b..43be69b2 100644 --- a/src/stories/Recipes/Layout/Layout.js +++ b/src/stories/Recipes/Layout/Layout.js @@ -193,6 +193,99 @@ const createLoginLayout = () => { ` } +const createUtilityLayout = () => { + return ` +
+ + + + +
+
+

Timeline with Icons

+
+
+
+ nature + + Buried by + Squirrel + +
+ Mar 31 +
+
+
+
+ eco + + Germinated in + nutrient-rich soil + +
+ May 28 +
+
+
+
+ forest + + Matured by + water + and + sunlight + +
+ Sep 14 +
+
+
+ +
+ + +
+

Timeline with Icons

+
+
+ nature + + Buried by + squirrel + +
+ Mar 31 +
+
+
+
+ eco + + Germinated in + nutrient-rich soil + +
+ May 28 +
+
+
+
+ forest + + Matured by + water + and + sunlight + +
+ Sep 14 +
+
+
+
+` +} + export const createLayout = ({ style = 'basic', rightSidebar = false }) => { if (style === 'basic') { return createBasicLayout() @@ -218,5 +311,9 @@ export const createLayout = ({ style = 'basic', rightSidebar = false }) => { return createLoginLayout() } + if (style === 'utility') { + return createUtilityLayout() + } + return `
` } diff --git a/src/stories/Recipes/Layout/Layout.mdx b/src/stories/Recipes/Layout/Layout.mdx index e3f5e09c..5031b815 100644 --- a/src/stories/Recipes/Layout/Layout.mdx +++ b/src/stories/Recipes/Layout/Layout.mdx @@ -176,3 +176,9 @@ A layout with a side panel looks like the following: A layout for a login page could look like the following: + +## Utility + +A layout explaining how the stack, split, and cluster utilities can be used to make flex layouts more readable. + + diff --git a/src/stories/Recipes/Layout/Layout.stories.js b/src/stories/Recipes/Layout/Layout.stories.js index c8d9ea48..f5c8f100 100644 --- a/src/stories/Recipes/Layout/Layout.stories.js +++ b/src/stories/Recipes/Layout/Layout.stories.js @@ -8,7 +8,7 @@ export default { argTypes: { style: { control: { type: 'select' }, - options: ['basic', 'sidebar', 'navbar', 'spinner', 'sidepanel', 'login'], + options: ['basic', 'sidebar', 'navbar', 'spinner', 'sidepanel', 'login', 'utility'], }, rightSidebar: { control: { type: 'boolean' }, @@ -62,3 +62,9 @@ export const Login = { style: 'login', }, } + +export const Utility = { + args: { + style: 'utility', + }, +} From 3f0cc60fa8d5c5e227d6c1f1cec4b0ce65731287 Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Mon, 8 Dec 2025 22:47:26 -0500 Subject: [PATCH 05/10] Add links to layout example and fix typos --- src/stories/Utilities/Cluster/Cluster.mdx | 5 ++++- src/stories/Utilities/Split/Split.mdx | 11 +++++++---- src/stories/Utilities/Stack/Stack.mdx | 3 +++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/stories/Utilities/Cluster/Cluster.mdx b/src/stories/Utilities/Cluster/Cluster.mdx index 028281ce..bdfa0b1d 100644 --- a/src/stories/Utilities/Cluster/Cluster.mdx +++ b/src/stories/Utilities/Cluster/Cluster.mdx @@ -16,13 +16,16 @@ While flex utilities are great for moving quickly or for simple layouts, they ca cumbersome to manage and tend to turn into "Flex Spaghetti". In some of these cases, reaching for named components may make sense. However, there are times when a full component is more restrictive than you need and hurts productivity. Sometimes you need just a bit more structure -to your layout, but not a component. Stacks provide a simple way to create readable, +to your layout, but not a component. Clusters provide a simple way to create readable, flexible layouts without the overhead of a full component. The cluster utility provides a simple way to create a wrapping, group of spaced items. It works with all of the gap and other flex utilities. It is effectively equivalent to using `.flex .flex-wrap .items-center .gap-md` together. +See [Utility Layout](?path=/docs/recipes-layout--docs#utility) for an example of how clusters +can be used to create more readable flex layouts. + ## Playground diff --git a/src/stories/Utilities/Split/Split.mdx b/src/stories/Utilities/Split/Split.mdx index 62b83f81..ad6892df 100644 --- a/src/stories/Utilities/Split/Split.mdx +++ b/src/stories/Utilities/Split/Split.mdx @@ -16,12 +16,15 @@ While flex utilities are great for moving quickly or for simple layouts, they ca cumbersome to manage and tend to turn into "Flex Spaghetti". In some of these cases, reaching for named components may make sense. However, there are times when a full component is more restrictive than you need and hurts productivity. Sometimes you need just a bit more structure -to your layout, but not a component. Stacks provide a simple way to create readable, +to your layout, but not a component. Splits provide a simple way to create readable, flexible layouts without the overhead of a full component. The split utility provides a simple way to push two items apart from each other. It is effectively equivalent to using `.flex .flex-wrap .items-center .justify-between .gap-md` together. +See [Utility Layout](?path=/docs/recipes-layout--docs#utility) for an example of how splits +can be used to create more readable flex layouts. + ## Playground @@ -29,13 +32,13 @@ It is effectively equivalent to using `.flex .flex-wrap .items-center .justify-b ## Without -A normal `div` without the cluster utility +A normal `div` without the split utility -## Cluster Property +## Split Property -`.cluster` Creates a flex cluster. +`.split` Creates a flex split. diff --git a/src/stories/Utilities/Stack/Stack.mdx b/src/stories/Utilities/Stack/Stack.mdx index 88367d5c..34687bb0 100644 --- a/src/stories/Utilities/Stack/Stack.mdx +++ b/src/stories/Utilities/Stack/Stack.mdx @@ -23,6 +23,9 @@ The stack utility provides a simple way to create a vertical stack of spaced ite works with all of the gap and other flex utilities. It is effectively equivalent to using `.flex .flex-col .gap-md` together. +See [Utility Layout](?path=/docs/recipes-layout--docs#utility) for an example of how stacks +can be used to create more readable flex layouts. + ## Playground From c72eb92b035f8db55c1d3f1464de49916a8d6569 Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Tue, 9 Dec 2025 11:12:28 -0500 Subject: [PATCH 06/10] Add document for recommended utilities usage --- .storybook/preview.js | 1 + src/stories/Utilities/Cluster/Cluster.mdx | 9 +-- src/stories/Utilities/Introduction.mdx | 90 +++++++++++++++++++++++ src/stories/Utilities/Split/Split.mdx | 8 +- src/stories/Utilities/Stack/Stack.mdx | 9 +-- 5 files changed, 94 insertions(+), 23 deletions(-) create mode 100644 src/stories/Utilities/Introduction.mdx diff --git a/.storybook/preview.js b/.storybook/preview.js index 53948d19..c2b8a4b0 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -23,6 +23,7 @@ const preview = { ['Base Reset', 'File Organization', 'Selective Imports', 'Tokens', 'Themes', 'Scale Overriding', 'Addons'], 'Tokens', 'Utilities', + ['Introduction'], 'Components', 'Recipes', ], diff --git a/src/stories/Utilities/Cluster/Cluster.mdx b/src/stories/Utilities/Cluster/Cluster.mdx index bdfa0b1d..b849177f 100644 --- a/src/stories/Utilities/Cluster/Cluster.mdx +++ b/src/stories/Utilities/Cluster/Cluster.mdx @@ -12,16 +12,9 @@ import { createSourceCodeLink } from '../../helpers/sourceCodeLink.js' }} > -While flex utilities are great for moving quickly or for simple layouts, they can easily become -cumbersome to manage and tend to turn into "Flex Spaghetti". In some of these cases, reaching -for named components may make sense. However, there are times when a full component is more -restrictive than you need and hurts productivity. Sometimes you need just a bit more structure -to your layout, but not a component. Clusters provide a simple way to create readable, -flexible layouts without the overhead of a full component. - The cluster utility provides a simple way to create a wrapping, group of spaced items. It works with all of the gap and other flex utilities. It is effectively equivalent to using -`.flex .flex-wrap .items-center .gap-md` together. +`.flex .flex-wrap .items-center .gap-md` together. See [Utility Introduction](?path=/docs/utilities-introduction--docs#higher-order-utilities-vs-components) for more information. See [Utility Layout](?path=/docs/recipes-layout--docs#utility) for an example of how clusters can be used to create more readable flex layouts. diff --git a/src/stories/Utilities/Introduction.mdx b/src/stories/Utilities/Introduction.mdx new file mode 100644 index 00000000..6141a507 --- /dev/null +++ b/src/stories/Utilities/Introduction.mdx @@ -0,0 +1,90 @@ +import { Meta } from '@storybook/addon-docs/blocks' +import { createSourceCodeLink } from '../helpers/sourceCodeLink.js' +import { createAlert } from '../Components/Alert/Alert.js' + + + +# Utilities Introduction + +
+ +Utility classes are simple CSS classes scoped to a simple style property like flex or gap. +They can be added together to style a portion of your HTML from scratch. +This is especially useful for quickly scaffolding out page layouts. + +Optics provides limited but high value utility classes. Utilities can often and easily lead to poorly written CSS, +inconsistent styling, or unresponsive layouts. However, there are cases where utilities can provide great value. +Optics aims to give you access to them, but also provide guidelines for how to use them most effectively. +The utilities Optics provides focus on solving quick scaffolding **(Layout and Position)** problems, +not typographical, color, or one-off fixes **(Look and Feel)**. + +We limit what utilities are made available to avoid maintaining a growing library of utilities, +keep our focus on intention revealing concepts and patterns, and keep maintainability top of mind. +Color, style, and typography should be named patterns. + +When using utilities, keep in mind that the intention of what style they apply may be clear, +but the intention of what the UI concept is trying to accomplish may not be. + +## When to use them? + +
+ +Simple flex based layouts, positioning items with gaps between them, or spiking how elements on the page will +be placed are great use cases for utilities. The value they provide is speed of use and not getting bogged down by having +to think about a meaningful name for every basic layout problem. + +Use before Reuse: Skipping straight to a named component may lead to spending more effort naming things before +it's necessary. Sometimes starting with utilities to get things out on the page is better. +When you notice repetition or a larger concept or idea, then refactor to a named component. + +## When to move utilities to semantically named UI concept? + +Utilities for layout problems is a great place to start but can suffer from a few problems as your project progresses. + +If a particular layout becomes a meaningful or repeated UI concept, thought should be put into naming it so as to provide consistency +and reveal the intention behind it. Utilities do not provide an intention-revealing name for what they do and thus if repeated or copied, +their intention can be lost. Copying between pages can unintentionally cause it to diverge from the original intent over time. + +When multiple breakpoints or container queries are needed this is another good opportunity to move from utilities to a named semantic concept. +This will allow for more complex layout to be managed within a named CSS class and maintain a simple semantic HTML structure. + +### Helpful rules of thumb to think about moving from utilities to a named concept: + +- If you are using 3 or more utilities, it might be a sign there is a larger concept at play that should be defined. +- If your layout needs to handle breakpoints or special responsive rules, utilities are going to get in the way of making this easy. You probably want to name the concept. +- If a particular collection of utilities is getting used together often, it may point to a UI pattern in your design that should be named. + +### Higher Order Utilities vs Components + +While flex utilities are great for moving quickly or for simple layouts, they can easily become +cumbersome to manage and tend to turn into "Flex Spaghetti". In some of these cases, reaching +for named components may make sense. However, there are times when a full component is more +restrictive than you need and hurts productivity. Sometimes you need just a bit more structure +to your layout, but not a component. [Stack](?path=/docs/utilities-stack--docs), [Cluster](?path=/docs/utilities-cluster--docs), and [Split](?path=/docs/utilities-split--docs) +provide a simple way to create readable, flexible layouts without the overhead of a full component. + +## A word of warning + +**The more componentized your UI is, the fewer utilities should be needed.** + +As mentioned above, Utilities are great for speed and spiking out concepts, +but they come with a set of warnings. They can be difficult to refactor, +especially if repeated across pages or sections, +they can be difficult to manage between screen sizes media and container breakpoints, +and they can cause HTML clutter. This is why it's important to know their strengths, +when to use them, and when to stop using them. diff --git a/src/stories/Utilities/Split/Split.mdx b/src/stories/Utilities/Split/Split.mdx index ad6892df..4cd0989c 100644 --- a/src/stories/Utilities/Split/Split.mdx +++ b/src/stories/Utilities/Split/Split.mdx @@ -12,15 +12,9 @@ import { createSourceCodeLink } from '../../helpers/sourceCodeLink.js' }} > -While flex utilities are great for moving quickly or for simple layouts, they can easily become -cumbersome to manage and tend to turn into "Flex Spaghetti". In some of these cases, reaching -for named components may make sense. However, there are times when a full component is more -restrictive than you need and hurts productivity. Sometimes you need just a bit more structure -to your layout, but not a component. Splits provide a simple way to create readable, -flexible layouts without the overhead of a full component. - The split utility provides a simple way to push two items apart from each other. It is effectively equivalent to using `.flex .flex-wrap .items-center .justify-between .gap-md` together. +See [Utility Introduction](?path=/docs/utilities-introduction--docs#higher-order-utilities-vs-components) for more information. See [Utility Layout](?path=/docs/recipes-layout--docs#utility) for an example of how splits can be used to create more readable flex layouts. diff --git a/src/stories/Utilities/Stack/Stack.mdx b/src/stories/Utilities/Stack/Stack.mdx index 34687bb0..e0291581 100644 --- a/src/stories/Utilities/Stack/Stack.mdx +++ b/src/stories/Utilities/Stack/Stack.mdx @@ -12,16 +12,9 @@ import { createSourceCodeLink } from '../../helpers/sourceCodeLink.js' }} > -While flex utilities are great for moving quickly or for simple layouts, they can easily become -cumbersome to manage and tend to turn into "Flex Spaghetti". In some of these cases, reaching -for named components may make sense. However, there are times when a full component is more -restrictive than you need and hurts productivity. Sometimes you need just a bit more structure -to your layout, but not a component. Stacks provide a simple way to create readable, -flexible layouts without the overhead of a full component. - The stack utility provides a simple way to create a vertical stack of spaced items. It works with all of the gap and other flex utilities. It is effectively equivalent to using -`.flex .flex-col .gap-md` together. +`.flex .flex-col .gap-md` together. See [Utility Introduction](?path=/docs/utilities-introduction--docs#higher-order-utilities-vs-components) for more information. See [Utility Layout](?path=/docs/recipes-layout--docs#utility) for an example of how stacks can be used to create more readable flex layouts. From cb8a9107bb32e926542495e97ce1582cdfc01604 Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Sat, 13 Dec 2025 19:37:01 -0500 Subject: [PATCH 07/10] Add op prefix to utilities --- src/core/utilities.css | 6 +++--- src/stories/Recipes/Layout/Layout.js | 18 +++++++++--------- src/stories/Utilities/Cluster/Cluster.js | 2 +- src/stories/Utilities/Cluster/Cluster.mdx | 2 +- src/stories/Utilities/Split/Split.js | 2 +- src/stories/Utilities/Split/Split.mdx | 2 +- src/stories/Utilities/Stack/Stack.js | 2 +- src/stories/Utilities/Stack/Stack.mdx | 2 +- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/core/utilities.css b/src/core/utilities.css index 809eb985..d5a94161 100644 --- a/src/core/utilities.css +++ b/src/core/utilities.css @@ -421,7 +421,7 @@ /* Advanced Layout Utilities */ /* Equivalent to .flex.flex-col.gap-md */ -.stack { +.op-stack { display: flex; flex-direction: column; @@ -431,7 +431,7 @@ } /* Equivalent to .flex.flex-wrap.items-center.gap-md */ -.cluster { +.op-cluster { display: flex; flex-wrap: wrap; @@ -445,7 +445,7 @@ } /* Equivalent to .flex.flex-wrap.items-center.justify-between.gap-md */ -.split { +.op-split { display: flex; flex-wrap: wrap; justify-content: space-between; diff --git a/src/stories/Recipes/Layout/Layout.js b/src/stories/Recipes/Layout/Layout.js index 43be69b2..0e8ae652 100644 --- a/src/stories/Recipes/Layout/Layout.js +++ b/src/stories/Recipes/Layout/Layout.js @@ -201,11 +201,11 @@ const createUtilityLayout = () => {
-
+

Timeline with Icons

-
-
-
+
+
+
nature Buried by @@ -215,8 +215,8 @@ const createUtilityLayout = () => { Mar 31
-
-
+
+
eco Germinated in @@ -226,8 +226,8 @@ const createUtilityLayout = () => { May 28
-
-
+
+
forest Matured by @@ -243,7 +243,7 @@ const createUtilityLayout = () => {
- +

Timeline with Icons

diff --git a/src/stories/Utilities/Cluster/Cluster.js b/src/stories/Utilities/Cluster/Cluster.js index 7df063b9..5a9e435a 100644 --- a/src/stories/Utilities/Cluster/Cluster.js +++ b/src/stories/Utilities/Cluster/Cluster.js @@ -4,7 +4,7 @@ export const createCluster = ({ cluster = true, alignItems = '', gap = '' }) => const wrapper = document.createElement('div') wrapper.style.height = '15rem' - wrapper.className = [cluster ? 'cluster' : '', alignItems ? `items-${alignItems}` : '', gap ? `gap-${gap}` : ''] + wrapper.className = [cluster ? 'op-cluster' : '', alignItems ? `items-${alignItems}` : '', gap ? `gap-${gap}` : ''] .filter(Boolean) .join(' ') diff --git a/src/stories/Utilities/Cluster/Cluster.mdx b/src/stories/Utilities/Cluster/Cluster.mdx index b849177f..40c64a93 100644 --- a/src/stories/Utilities/Cluster/Cluster.mdx +++ b/src/stories/Utilities/Cluster/Cluster.mdx @@ -32,7 +32,7 @@ A normal `div` without the cluster utility ## Cluster Property -`.cluster` Creates a flex cluster. +`.op-cluster` Creates a flex cluster. diff --git a/src/stories/Utilities/Split/Split.js b/src/stories/Utilities/Split/Split.js index 64875565..eedb2741 100644 --- a/src/stories/Utilities/Split/Split.js +++ b/src/stories/Utilities/Split/Split.js @@ -4,7 +4,7 @@ export const createSplit = ({ split = true, alignItems = '', gap = '' }) => { const wrapper = document.createElement('div') wrapper.style.height = '10rem' - wrapper.className = [split ? 'split' : '', alignItems ? `items-${alignItems}` : '', gap ? `gap-${gap}` : ''] + wrapper.className = [split ? 'op-split' : '', alignItems ? `items-${alignItems}` : '', gap ? `gap-${gap}` : ''] .filter(Boolean) .join(' ') diff --git a/src/stories/Utilities/Split/Split.mdx b/src/stories/Utilities/Split/Split.mdx index 4cd0989c..3511bfce 100644 --- a/src/stories/Utilities/Split/Split.mdx +++ b/src/stories/Utilities/Split/Split.mdx @@ -32,7 +32,7 @@ A normal `div` without the split utility ## Split Property -`.split` Creates a flex split. +`.op-split` Creates a flex split. diff --git a/src/stories/Utilities/Stack/Stack.js b/src/stories/Utilities/Stack/Stack.js index a8dafee5..68a6e2c3 100644 --- a/src/stories/Utilities/Stack/Stack.js +++ b/src/stories/Utilities/Stack/Stack.js @@ -3,7 +3,7 @@ import { createChildren } from '../../helpers/utils' export const createStack = ({ stack = true, alignItems = '', gap = '' }) => { const wrapper = document.createElement('div') - wrapper.className = [stack ? 'stack' : '', alignItems ? `items-${alignItems}` : '', gap ? `gap-${gap}` : ''] + wrapper.className = [stack ? 'op-stack' : '', alignItems ? `items-${alignItems}` : '', gap ? `gap-${gap}` : ''] .filter(Boolean) .join(' ') diff --git a/src/stories/Utilities/Stack/Stack.mdx b/src/stories/Utilities/Stack/Stack.mdx index e0291581..5d0a4e50 100644 --- a/src/stories/Utilities/Stack/Stack.mdx +++ b/src/stories/Utilities/Stack/Stack.mdx @@ -32,7 +32,7 @@ A normal `div` without the stack utility ## Stack Property -`.stack` Creates a flex stack. +`.op-stack` Creates a flex stack. From 07c7b0c21c1d66bb42bfd8bc35359263edd55de9 Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Sat, 13 Dec 2025 19:38:15 -0500 Subject: [PATCH 08/10] Add comment about prefix --- src/stories/Utilities/Cluster/Cluster.mdx | 3 +++ src/stories/Utilities/Split/Split.mdx | 3 +++ src/stories/Utilities/Stack/Stack.mdx | 3 +++ 3 files changed, 9 insertions(+) diff --git a/src/stories/Utilities/Cluster/Cluster.mdx b/src/stories/Utilities/Cluster/Cluster.mdx index 40c64a93..4cffcaf0 100644 --- a/src/stories/Utilities/Cluster/Cluster.mdx +++ b/src/stories/Utilities/Cluster/Cluster.mdx @@ -19,6 +19,9 @@ works with all of the gap and other flex utilities. It is effectively equivalent See [Utility Layout](?path=/docs/recipes-layout--docs#utility) for an example of how clusters can be used to create more readable flex layouts. +Note: This utilities uses the `op` prefix to avoid potential naming conflicts with other CSS frameworks. +This is a pattern we hope to move towards for all utilities in the future. + ## Playground diff --git a/src/stories/Utilities/Split/Split.mdx b/src/stories/Utilities/Split/Split.mdx index 3511bfce..944e0130 100644 --- a/src/stories/Utilities/Split/Split.mdx +++ b/src/stories/Utilities/Split/Split.mdx @@ -19,6 +19,9 @@ See [Utility Introduction](?path=/docs/utilities-introduction--docs#higher-order See [Utility Layout](?path=/docs/recipes-layout--docs#utility) for an example of how splits can be used to create more readable flex layouts. +Note: This utilities uses the `op` prefix to avoid potential naming conflicts with other CSS frameworks. +This is a pattern we hope to move towards for all utilities in the future. + ## Playground diff --git a/src/stories/Utilities/Stack/Stack.mdx b/src/stories/Utilities/Stack/Stack.mdx index 5d0a4e50..0cfe7af8 100644 --- a/src/stories/Utilities/Stack/Stack.mdx +++ b/src/stories/Utilities/Stack/Stack.mdx @@ -19,6 +19,9 @@ works with all of the gap and other flex utilities. It is effectively equivalent See [Utility Layout](?path=/docs/recipes-layout--docs#utility) for an example of how stacks can be used to create more readable flex layouts. +Note: This utilities uses the `op` prefix to avoid potential naming conflicts with other CSS frameworks. +This is a pattern we hope to move towards for all utilities in the future. + ## Playground From 2381aff0a0fdd099e9217075ae78787ce78eb21e Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Sat, 13 Dec 2025 19:38:43 -0500 Subject: [PATCH 09/10] Use where selector to provide defaults for gap and alignment that can be overridden with other utilities --- src/core/utilities.css | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/core/utilities.css b/src/core/utilities.css index d5a94161..4664f617 100644 --- a/src/core/utilities.css +++ b/src/core/utilities.css @@ -424,24 +424,21 @@ .op-stack { display: flex; flex-direction: column; +} - &:not([class*='gap-']) { - gap: var(--op-space-medium); - } +:where(.op-stack) { + gap: var(--op-space-medium); } /* Equivalent to .flex.flex-wrap.items-center.gap-md */ .op-cluster { display: flex; flex-wrap: wrap; +} - &:not([class*='items-']) { - align-items: center; - } - - &:not([class*='gap-']) { - gap: var(--op-space-medium); - } +:where(.op-cluster) { + align-items: center; + gap: var(--op-space-medium); } /* Equivalent to .flex.flex-wrap.items-center.justify-between.gap-md */ @@ -449,12 +446,9 @@ display: flex; flex-wrap: wrap; justify-content: space-between; +} - &:not([class*='items-']) { - align-items: center; - } - - &:not([class*='gap-']) { - gap: var(--op-space-medium); - } +:where(.op-split) { + align-items: center; + gap: var(--op-space-medium); } From b102879eb49ed35ddb11a0b9b85d1bbba123fbef Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Sat, 13 Dec 2025 19:47:27 -0500 Subject: [PATCH 10/10] Fix typo --- src/stories/Utilities/Cluster/Cluster.mdx | 2 +- src/stories/Utilities/Split/Split.mdx | 2 +- src/stories/Utilities/Stack/Stack.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stories/Utilities/Cluster/Cluster.mdx b/src/stories/Utilities/Cluster/Cluster.mdx index 4cffcaf0..0ea45cb6 100644 --- a/src/stories/Utilities/Cluster/Cluster.mdx +++ b/src/stories/Utilities/Cluster/Cluster.mdx @@ -19,7 +19,7 @@ works with all of the gap and other flex utilities. It is effectively equivalent See [Utility Layout](?path=/docs/recipes-layout--docs#utility) for an example of how clusters can be used to create more readable flex layouts. -Note: This utilities uses the `op` prefix to avoid potential naming conflicts with other CSS frameworks. +Note: This utility uses the `op` prefix to avoid potential naming conflicts with other CSS frameworks. This is a pattern we hope to move towards for all utilities in the future. ## Playground diff --git a/src/stories/Utilities/Split/Split.mdx b/src/stories/Utilities/Split/Split.mdx index 944e0130..fd29fb50 100644 --- a/src/stories/Utilities/Split/Split.mdx +++ b/src/stories/Utilities/Split/Split.mdx @@ -19,7 +19,7 @@ See [Utility Introduction](?path=/docs/utilities-introduction--docs#higher-order See [Utility Layout](?path=/docs/recipes-layout--docs#utility) for an example of how splits can be used to create more readable flex layouts. -Note: This utilities uses the `op` prefix to avoid potential naming conflicts with other CSS frameworks. +Note: This utility uses the `op` prefix to avoid potential naming conflicts with other CSS frameworks. This is a pattern we hope to move towards for all utilities in the future. ## Playground diff --git a/src/stories/Utilities/Stack/Stack.mdx b/src/stories/Utilities/Stack/Stack.mdx index 0cfe7af8..ca014144 100644 --- a/src/stories/Utilities/Stack/Stack.mdx +++ b/src/stories/Utilities/Stack/Stack.mdx @@ -19,7 +19,7 @@ works with all of the gap and other flex utilities. It is effectively equivalent See [Utility Layout](?path=/docs/recipes-layout--docs#utility) for an example of how stacks can be used to create more readable flex layouts. -Note: This utilities uses the `op` prefix to avoid potential naming conflicts with other CSS frameworks. +Note: This utility uses the `op` prefix to avoid potential naming conflicts with other CSS frameworks. This is a pattern we hope to move towards for all utilities in the future. ## Playground