Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
989fd6c
chore: πŸ€– add pkg vite-plugin-static-copy
punkbit Mar 16, 2026
f8480ed
chore: πŸ€– use vite setup to copy css to distribution
punkbit Mar 16, 2026
6498f65
refactor: πŸ’‘ make static copy process common for cjs and esm
punkbit Mar 16, 2026
3a2a704
docs: πŸ“ about css modules
punkbit Mar 16, 2026
dc46845
style: πŸ’„ add Button css module
punkbit Mar 16, 2026
3fc0b07
style: πŸ’„ assign css module to component Button
punkbit Mar 16, 2026
9e7c16b
chore: πŸ€– add class-variance-authority and clsx
punkbit Mar 16, 2026
e76c05e
style: πŸ’„ migrate styled component to css modules
punkbit Mar 16, 2026
9cbbd0a
style: πŸ’„ migrate styled component to css modules
punkbit Mar 16, 2026
1fa943e
refactor: πŸ’‘ token generator
punkbit Mar 16, 2026
bbeb9f4
chore: πŸ€– storybook support for css modules
punkbit Mar 16, 2026
380fbaa
chore: πŸ€– add button stories for each state, e.g. primary, secondary, etc
punkbit Mar 16, 2026
35f64da
chore: πŸ€– add changeset
punkbit Mar 16, 2026
2e962fa
chore: πŸ€– remove comment
punkbit Mar 16, 2026
6eb6ae7
chore: πŸ€– update changeset
punkbit Mar 16, 2026
05c5bb5
chore: πŸ€– declare css files as sideEffects
punkbit Mar 16, 2026
6968188
refactor: πŸ’‘ make class utility libraries, e.g. cva; actual dependencies
punkbit Mar 16, 2026
843502b
chore: πŸ€– add TODO button
punkbit Mar 16, 2026
7281cdd
style: πŸ’„ possible typo, e.g. make similar bg size as others
punkbit Mar 16, 2026
2c9be4f
refactor: πŸ’‘ role button is redundant per the ARIA in HTML spec, add a…
punkbit Mar 16, 2026
c43c714
chore: πŸ€– format
punkbit Mar 16, 2026
7adf448
refactor: πŸ’‘ generate tokens, e.g. theme config shared file as json, r…
punkbit Mar 16, 2026
8ed0aba
chore: πŸ€– add TODO regarding removal of styled components for the futu…
punkbit Mar 16, 2026
3c75867
chore: πŸ€– add migration note to changeset
punkbit Mar 16, 2026
eada9e0
refactor: πŸ’‘ forward props (delegated)
punkbit Mar 16, 2026
2e2791c
fix: πŸ› checkout design tokens from main
punkbit Mar 16, 2026
78ba449
fix: πŸ› if a consumer passes className, it would overwrite the variant…
punkbit Mar 16, 2026
2898d94
fix: πŸ› avoid double colmns
punkbit Mar 16, 2026
6d31da6
chore: πŸ€– regenerate tokens
punkbit Mar 16, 2026
6f2919e
fix: πŸ› replace missing by global focus token
punkbit Mar 16, 2026
d270807
fix: πŸ› focus rings restored for keyboard users
punkbit Mar 16, 2026
7d01418
refactor: πŸ’‘ follow BEM naming convention
punkbit Mar 16, 2026
9585f36
chore: πŸ€– include BEM rule in llm conventions
punkbit Mar 16, 2026
d13f1a9
fix: πŸ› empty token
punkbit Mar 16, 2026
f6988dc
fix: πŸ› .button > span selector to explicit .button__label class
punkbit Mar 16, 2026
376aad8
chore: πŸ€– format
punkbit Mar 16, 2026
6ab18dd
fix: πŸ› claude review, missing prefers-reduced-motion support for the …
punkbit Mar 17, 2026
e51051d
fix: πŸ› add line
punkbit Mar 17, 2026
ac9aaa9
refactor: πŸ’‘ remove sideEffects redundancy
punkbit Apr 7, 2026
e6f8aca
fix: πŸ› package.json typo
punkbit Apr 7, 2026
ae9b0b2
style: πŸ’„ missing background repeat, as commented by claude PR
punkbit Apr 7, 2026
dc3dbf8
fix: πŸ› Changed both attributes to use || undefined:
punkbit Apr 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/eight-colts-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@clickhouse/click-ui': minor
---

This library will now use CSS Modules for styling and because it's distributed unbundled, gives the consumer application full control over bundling and optimisations. You'll only include what you actually use, resulting in smaller bundle sizes and better performance!

**Migration:**

Your bundler must be configured to handle `.module.css` imports from `node_modules`. Most popular bundlers (Vite, webpack, Parcel, Rollup with appropriate plugins) support CSS Modules by default or with minimal configuration.

NOTE: We're currently migrating from Styled-Components to CSS Modules. Some components may still use Styled-Components during the transition period.

To learn more about CSS modules support, check our documentation [here](https://github.com/ClickHouse/click-ui?tab=readme-ov-file#css-modules)
20 changes: 20 additions & 0 deletions .llm/CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ export interface ComponentProps extends React.HTMLAttributes<HTMLElement> {
- Use transient props (prefixed with `$`) for styled-component internal state
- Use `data-*` attributes for styling hooks instead of generated class names

### CSS Modules (BEM Naming)

When using CSS Modules (migration in progress from styled-components):

- **Follow BEM naming convention**:
- `.button` - Block (component root)
- `.button__icon` - Element (child of block, use double underscore)
- `.button--primary` - Modifier (variant/state, use double dash)
- `.button--primary:hover` - State pseudo-classes

- **Example structure**:
```css
.button { /* base styles */ }
.button__icon { /* icon element */ }
.button--primary { /* primary variant */ }
.button--primary:focus-visible { /* keyboard focus state */ }
```
- Use CSS custom properties from theme tokens: `var(--click-button-basic-color-primary-background-default)`
- Always include `:focus-visible` styles for keyboard accessibility, never use `outline: none` without replacement

### Accessibility (Mandatory)

- Interactive elements need `role`, `aria-label`, `aria-describedby`
Expand Down
50 changes: 46 additions & 4 deletions .scripts/js/generate-tokens.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { register } from '@tokens-studio/sd-transforms';
import StyleDictionary from 'style-dictionary';
import config from '../../src/theme/theme.config.json' with { type: 'json' };

const themes = ['dark', 'light'];
const THEME_DATA_ATTRIBUTE = `data-${config.storageKey}`;

await register(StyleDictionary);

Expand All @@ -17,6 +19,38 @@ StyleDictionary.registerTransform({
},
});

StyleDictionary.registerTransform({
type: 'name',
name: 'name/cti/kebab',
transform: (token, options) => {
if (options.prefix && options.prefix.length) {
return [options.prefix].concat(token.path).join('-');
} else {
return token.path.join('-');
}
},
});

StyleDictionary.registerFormat({
name: 'css/themed-variables',
format: function ({ dictionary, file }) {
const themeName = file.destination.replace('tokens-', '').replace('.css', '');
const tokens = dictionary.allTokens
.map(token => {
const varName = token.path.join('-');
const cleanValue = String(token.value).replace(/;+$/, '');
return ` --${varName}: ${cleanValue};`;
})
.join('\n');

if (themeName === 'light') {
return `:root,\n[${THEME_DATA_ATTRIBUTE}="light"] {\n${tokens}\n}`;
} else {
return `[${THEME_DATA_ATTRIBUTE}="dark"] {\n${tokens}\n}`;
}
},
});

StyleDictionary.registerFormat({
name: 'typescript/es6-theme',
format: function ({ dictionary, file }) {
Expand Down Expand Up @@ -46,10 +80,7 @@ StyleDictionary.registerFormat({

for (const theme of themes) {
const sd = new StyleDictionary({
source: [
`./tokens/**/!(${themes.join('|')}).json`,
`./tokens/**/${theme}.json`,
],
source: [`./tokens/**/!(${themes.join('|')}).json`, `./tokens/**/${theme}.json`],
preprocessors: ['tokens-studio'],
platforms: {
ts: {
Expand All @@ -63,6 +94,17 @@ for (const theme of themes) {
},
],
},
css: {
transformGroup: 'tokens-studio',
transforms: ['name/cti/kebab'],
buildPath: 'src/theme/styles/',
files: [
{
destination: `tokens-${theme}.css`,
format: 'css/themed-variables',
},
],
},
},
});

Expand Down
7 changes: 4 additions & 3 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ const config: StorybookConfig = {
},

async viteFinal(config, { configType }) {
// Workaround for Storybook 10.0.7 bug where MDX files generate file:// imports
// See: https://github.com/storybookjs/storybook/issues (mdx-react-shim resolution)
config.plugins = config.plugins || [];
config.plugins = (config.plugins || []).filter((plugin) => {
const pluginName = plugin && typeof plugin === 'object' && 'name' in plugin ? plugin.name : null;
return pluginName !== 'css-external';
});
config.plugins.push({
name: 'fix-storybook-mdx-shim',
resolveId(source) {
Expand Down
79 changes: 39 additions & 40 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { useState, useEffect, ReactNode } from 'react';
import type { Preview } from '@storybook/react-vite';
import { Decorator } from '@storybook/react-vite';
import { styled } from 'styled-components';
import { themes } from 'storybook/theming';
import { ClickUIProvider } from '@/providers';
import { useState, useEffect, ReactNode } from "react";
import type { Preview } from "@storybook/react-vite";
import { Decorator } from "@storybook/react-vite";
import { styled } from "styled-components";
import { themes } from "storybook/theming";
import { ClickUIProvider } from "../src/providers";

const ThemeBlock = styled.div<{ $left?: boolean; $bfill?: boolean }>(
({ $left, $bfill: fill, theme }) => `
position: absolute;
top: 0.5rem;
left: ${$left || fill ? 0 : '50vw'};
left: ${$left || fill ? 0 : "50vw"};
right: 0;
height: fit-content;
bottom: 0;
Expand All @@ -22,26 +22,28 @@ const ThemeBlock = styled.div<{ $left?: boolean; $bfill?: boolean }>(

export const globalTypes = {
theme: {
name: 'Theme',
description: 'Global theme for components',
defaultValue: 'system',
name: "Theme",
description: "Global theme for components",
defaultValue: "system",
toolbar: {
icon: 'circlehollow',
icon: "circlehollow",
items: [
{ value: 'system', icon: 'browser', title: 'system' },
{ value: 'dark', icon: 'moon', title: 'dark' },
{ value: 'light', icon: 'sun', title: 'light' },
{ value: "system", icon: "browser", title: "system" },
{ value: "dark", icon: "moon", title: "dark" },
{ value: "light", icon: "sun", title: "light" },
],
showName: true,
},
},
};

const getSystemTheme = (): 'dark' | 'light' => {
if (typeof window !== 'undefined' && window.matchMedia) {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const getSystemTheme = (): "dark" | "light" => {
if (typeof window !== "undefined" && window.matchMedia) {
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
return 'dark';
return "dark";
};

interface ThemeWrapperProps {
Expand All @@ -50,27 +52,24 @@ interface ThemeWrapperProps {
}

const ThemeWrapper = ({ themeSelection, children }: ThemeWrapperProps) => {
const [systemTheme, setSystemTheme] = useState<'dark' | 'light'>(getSystemTheme);
const [systemTheme, setSystemTheme] = useState<"dark" | "light">(getSystemTheme);

// Listen for system theme changes
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = () => {
setSystemTheme(mediaQuery.matches ? 'dark' : 'light');
setSystemTheme(mediaQuery.matches ? "dark" : "light");
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, []);

// Resolve the actual theme: handle "system" and fallback for undefined/null
const theme =
themeSelection === 'system' || !themeSelection ? systemTheme : themeSelection;
themeSelection === "system" || !themeSelection ? systemTheme : themeSelection;

return (
<ClickUIProvider
theme={theme}
config={{ tooltip: { delayDuration: 0 } }}
>
<ClickUIProvider theme={theme} config={{ tooltip: { delayDuration: 0 } }}>
<ThemeBlock $left>{children}</ThemeBlock>
</ClickUIProvider>
);
Expand All @@ -91,22 +90,22 @@ const preview: Preview = {
parameters: {
options: {
storySort: {
method: 'alphabetical',
method: "alphabetical",
order: [
'Introduction',
'Buttons',
'Cards',
'Layout',
'Forms',
'Display',
'Sidebar',
'Typography',
'Colors',
['Title', 'Text', 'Link'],
"Introduction",
"Buttons",
"Cards",
"Layout",
"Forms",
"Display",
"Sidebar",
"Typography",
"Colors",
["Title", "Text", "Link"],
],
},
},
actions: { argTypesRegex: '^on[A-Z].*' },
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ You can find the official docs for the Click UI design system and component libr
- [Generating design tokens](#generating-design-tokens)
- [Local development](#local-development)
- [Circular dependency check](#circular-dependency-check)
- [CSS Modules](#css-modules)
* [Tests](#Tests)
- [Functional tests](#functional-tests)
- [Visual regression tests](#visual-regression-tests)
Expand Down Expand Up @@ -144,6 +145,39 @@ By avoiding local preview files, we ensure that component experimentation happen

To get started with the development playground, refer to the Storybook section [here](#storybook).

### CSS Modules

This library uses [CSS Modules](https://github.com/css-modules/css-modules) for styling and is distributed unbundled, giving your application full control over bundling and optimizations. This means you only include what you actually use, resulting in smaller bundle sizes and better performance!

Most modern React frameworks support CSS Modules out of the box, including Next.js, Vite, Create React App, and TanStack Start, with no configuration required.

> [!NOTE]
> We're currently migrating from Styled-Components to CSS Modules. Some components may still use Styled-Components during this transition period.
#### Benefits

CSS Modules align naturally with component-level imports. When you import a component like `Button`, its `Button.module.css` is automatically included. If you don't use the component, neither the JavaScript, or CSS will be bundled in your application's output. Only the necessary stylesheets will be included in the output bundle.

#### Custom Build Configurations

Although most modern React setups have CSS Modules built-in, if your build tool doesn't support it by default, you'll need to configure it.

Let's assume you have an old Webpack setup. Here's an example of how that'd look like:

```js
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: { modules: true }
}
]
}
```

For other bundlers, refer to their documentation on CSS Modules configuration.

## Tests

### Functional tests
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"sideEffects": false,
"sideEffects": [
"**/*.css"
],
"exports": {
".": {
"types": "./dist/types/index.d.ts",
Expand Down Expand Up @@ -410,6 +412,8 @@
"@radix-ui/react-tabs": "1.1.1",
"@radix-ui/react-toast": "1.2.2",
"@radix-ui/react-tooltip": "1.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.19",
"lodash-es": "^4.17.23",
"react-sortablejs": "^6.1.4",
Expand Down Expand Up @@ -470,6 +474,7 @@
"vite": "^7.3.0",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-externalize-deps": "^0.10.0",
"vite-plugin-static-copy": "^3.2.0",
"vite-tsconfig-paths": "^6.0.5",
"vitest": "^2.1.8",
"watch": "^1.0.2"
Expand Down
Loading
Loading