diff --git a/.changeset/remove-styled-components.md b/.changeset/remove-styled-components.md
new file mode 100644
index 000000000..50a08841c
--- /dev/null
+++ b/.changeset/remove-styled-components.md
@@ -0,0 +1,11 @@
+---
+'@clickhouse/click-ui': major
+---
+
+Remove styled-components as a dependency. click-ui no longer uses styled-components internally — every component renders from CSS Modules, and the runtime theme is now delivered through click-ui's own React context (via `ClickUIProvider`).
+
+Breaking changes:
+
+- `styled-components` is no longer a peer dependency. If your app doesn't use styled-components for its own code, you can remove it.
+- The deprecated `linkStyles` and `StyledLinkProps` exports have been removed. Use the `Link` component with the `component` prop instead — e.g. `text`.
+- The lower-level `ThemeProvider` export has been removed; `ClickUIProvider` is now the single provider. It already set the theme (and wraps toast/tooltip), so replace any bare `` with ``.
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index 07a324dbb..27de30ff3 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -1,24 +1,6 @@
-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"};
- right: 0;
- height: fit-content;
- bottom: 0;
- overflow: auto;
- padding: 1rem;
- box-sizing: border-box;
- background: ${theme.click.storybook.global.background};
- `
-);
+import { withTheme } from "./withThemeDecorator";
export const globalTypes = {
theme: {
@@ -37,55 +19,6 @@ export const globalTypes = {
},
};
-const getSystemTheme = (): "dark" | "light" => {
- if (typeof window !== "undefined" && window.matchMedia) {
- return window.matchMedia("(prefers-color-scheme: dark)").matches
- ? "dark"
- : "light";
- }
- return "dark";
-};
-
-interface ThemeWrapperProps {
- themeSelection: string | undefined;
- children: ReactNode;
-}
-
-const ThemeWrapper = ({ themeSelection, children }: ThemeWrapperProps) => {
- const [systemTheme, setSystemTheme] = useState<"dark" | "light">(getSystemTheme);
-
- // Listen for system theme changes
- useEffect(() => {
- const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
- const handleChange = () => {
- setSystemTheme(mediaQuery.matches ? "dark" : "light");
- };
- 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;
-
- return (
-
- {children}
-
- );
-};
-
-const withTheme: Decorator = (StoryFn, context) => {
- const parameters = context.parameters;
- const themeSelection = parameters?.theme || context.globals.theme;
-
- return (
-
-
-
- );
-};
-
const preview: Preview = {
parameters: {
options: {
diff --git a/.storybook/withThemeDecorator.tsx b/.storybook/withThemeDecorator.tsx
new file mode 100644
index 000000000..0305a298e
--- /dev/null
+++ b/.storybook/withThemeDecorator.tsx
@@ -0,0 +1,83 @@
+import { useState, useEffect, ReactNode } from "react";
+import { Decorator } from "@storybook/react-vite";
+import { ClickUIProvider } from "../src/providers";
+import { useTheme } from "../src/theme/ThemeContext";
+
+const ThemeBlock = ({
+ left,
+ bfill,
+ children,
+}: {
+ left?: boolean;
+ bfill?: boolean;
+ children: ReactNode;
+}) => {
+ const theme = useTheme();
+ return (
+
+ {children}
+
+ );
+};
+
+const getSystemTheme = (): "dark" | "light" => {
+ if (typeof window !== "undefined" && window.matchMedia) {
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "light";
+ }
+ return "dark";
+};
+
+interface ThemeWrapperProps {
+ themeSelection: string | undefined;
+ children: ReactNode;
+}
+
+const ThemeWrapper = ({ themeSelection, children }: ThemeWrapperProps) => {
+ const [systemTheme, setSystemTheme] = useState<"dark" | "light">(getSystemTheme);
+
+ // Listen for system theme changes
+ useEffect(() => {
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
+ const handleChange = () => {
+ setSystemTheme(mediaQuery.matches ? "dark" : "light");
+ };
+ 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;
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const withTheme: Decorator = (StoryFn, context) => {
+ const parameters = context.parameters;
+ const themeSelection = parameters?.theme || context.globals.theme;
+
+ return (
+
+
+
+ );
+};
diff --git a/package.json b/package.json
index 8febea6f0..93e34284b 100644
--- a/package.json
+++ b/package.json
@@ -424,8 +424,7 @@
"react-syntax-highlighter": "^16.1.0",
"react-virtualized-auto-sizer": "^1.0.20",
"react-window": "^1.8.9",
- "sortablejs": "^1.15.0",
- "styled-components": "^6.1.11"
+ "sortablejs": "^1.15.0"
},
"devDependencies": {
"@changesets/cli": "^2.29.8",
@@ -450,7 +449,6 @@
"@types/react-window": "^1.8.8",
"@types/sortablejs": "^1.15.2",
"@vitejs/plugin-react": "^5.1.2",
- "babel-plugin-styled-components": "^2.1.4",
"chromatic": "^13.3.4",
"eslint": "^9",
"eslint-import-resolver-typescript": "^4.4.4",
@@ -492,13 +490,11 @@
"peerDependencies": {
"dayjs": "^1.11.19",
"react": "^18.3.1 || ^19.0.0",
- "react-dom": "^18.3.1 || ^19.0.0",
- "styled-components": "^6.1.11"
+ "react-dom": "^18.3.1 || ^19.0.0"
},
"resolutions": {
"@types/react": "18.3.1",
- "@types/react-dom": "18.3.1",
- "styled-components": "6.1.11"
+ "@types/react-dom": "18.3.1"
},
"packageManager": "yarn@4.5.3",
"engines": {
diff --git a/src/components/Assets/Flags/system/Flag.tsx b/src/components/Assets/Flags/system/Flag.tsx
index 0d8556762..109858357 100644
--- a/src/components/Assets/Flags/system/Flag.tsx
+++ b/src/components/Assets/Flags/system/Flag.tsx
@@ -1,5 +1,5 @@
import { SVGAttributes } from 'react';
-import { useTheme } from 'styled-components';
+import { useTheme } from '@/theme/ThemeContext';
import { getFallbackThemeName } from '@/theme/theme.utils';
import { SvgImageElement } from '@/components/Icon/SvgImageElement';
import { FlagName, FlagProps } from './types';
diff --git a/src/components/Assets/Icons/system/Icon.tsx b/src/components/Assets/Icons/system/Icon.tsx
index 42bc8ede7..999ca4fa2 100644
--- a/src/components/Assets/Icons/system/Icon.tsx
+++ b/src/components/Assets/Icons/system/Icon.tsx
@@ -1,5 +1,5 @@
import { SVGAttributes } from 'react';
-import { useTheme } from 'styled-components';
+import { useTheme } from '@/theme/ThemeContext';
import { getFallbackThemeName } from '@/theme/theme.utils';
import { SvgImageElement } from '@/components/Icon/SvgImageElement';
import { IconName, IconProps } from './types';
diff --git a/src/components/Assets/Logos/system/Logo.tsx b/src/components/Assets/Logos/system/Logo.tsx
index 49b937550..a054813c2 100644
--- a/src/components/Assets/Logos/system/Logo.tsx
+++ b/src/components/Assets/Logos/system/Logo.tsx
@@ -1,5 +1,5 @@
import { SVGAttributes } from 'react';
-import { useTheme } from 'styled-components';
+import { useTheme } from '@/theme/ThemeContext';
import { getFallbackThemeName } from '@/theme/theme.utils';
import { SvgImageElement } from '@/components/Icon/SvgImageElement';
import { LogoName, LogoProps } from './types';
diff --git a/src/components/Assets/Payments/system/Payment.tsx b/src/components/Assets/Payments/system/Payment.tsx
index 291ee2a83..9f38a5cf5 100644
--- a/src/components/Assets/Payments/system/Payment.tsx
+++ b/src/components/Assets/Payments/system/Payment.tsx
@@ -1,5 +1,5 @@
import { SVGAttributes } from 'react';
-import { useTheme } from 'styled-components';
+import { useTheme } from '@/theme/ThemeContext';
import { getFallbackThemeName } from '@/theme/theme.utils';
import { SvgImageElement } from '@/components/Icon/SvgImageElement';
import { PaymentName, PaymentProps } from './types';
diff --git a/src/components/Badge/Badge.module.css b/src/components/Badge/Badge.module.css
index c90a072cd..b16ae0849 100644
--- a/src/components/Badge/Badge.module.css
+++ b/src/components/Badge/Badge.module.css
@@ -108,9 +108,9 @@
font: var(--click-badge-typography-label-md-default);
}
-/* Content row — mirrors the inner Content styled component. The original
- `Content` was never passed `$size`, so its `gap` always defaulted to the
- md token regardless of the badge's actual `size`. Preserved verbatim. */
+/* Content row `gap` always uses the md token regardless of the badge's actual
+ `size` — matching the pre-migration behavior, where the row was never passed
+ a size. Preserved verbatim. */
.content {
display: inline-flex;
@@ -120,28 +120,25 @@
gap: var(--click-badge-space-md-gap);
}
-/* BadgeContent — wraps the IconWrapper. The original styled(IconWrapper) set
- `width: auto; overflow: hidden;` on the IconWrapper's rendered root and
- styled the descendant