diff --git a/.changeset/fix-codeblock-button-overlap.md b/.changeset/fix-codeblock-button-overlap.md
new file mode 100644
index 000000000..c4155c3c1
--- /dev/null
+++ b/.changeset/fix-codeblock-button-overlap.md
@@ -0,0 +1,5 @@
+---
+"@clickhouse/click-ui": patch
+---
+
+CodeBlock action buttons (copy, wrap) are now hidden by default and revealed on hover or focus-within with a fade transition. Buttons display a semi-opaque background pill matching the code block theme to prevent overlap with code content, and expose descriptive `aria-label`s (with `aria-pressed` on the wrap toggle) for screen reader users. IconButton now honours a caller-supplied `aria-label`, falling back to the icon name when none is provided.
diff --git a/src/components/CodeBlock/CodeBlock.test.tsx b/src/components/CodeBlock/CodeBlock.test.tsx
new file mode 100644
index 000000000..f031ba2ab
--- /dev/null
+++ b/src/components/CodeBlock/CodeBlock.test.tsx
@@ -0,0 +1,40 @@
+import { CodeBlock } from '@/components/CodeBlock';
+import { fireEvent } from '@testing-library/react';
+import { renderCUI } from '@/utils/test-utils';
+
+describe('CodeBlock', () => {
+ it('exposes accessible labels for the action buttons', () => {
+ const { getByRole } = renderCUI(
+
+ SELECT 1
+
+ );
+
+ expect(getByRole('button', { name: 'Copy code' })).toBeInTheDocument();
+ expect(getByRole('button', { name: 'Wrap long lines' })).toBeInTheDocument();
+ });
+
+ it('toggles the wrap button label and pressed state when clicked', () => {
+ const { getByRole } = renderCUI(
+
+ SELECT 1
+
+ );
+
+ const wrapButton = getByRole('button', { name: 'Wrap long lines' });
+ expect(wrapButton).toHaveAttribute('aria-pressed', 'false');
+
+ fireEvent.click(wrapButton);
+
+ expect(getByRole('button', { name: 'Disable line wrapping' })).toHaveAttribute(
+ 'aria-pressed',
+ 'true'
+ );
+ });
+});
diff --git a/src/components/CodeBlock/CodeBlock.tsx b/src/components/CodeBlock/CodeBlock.tsx
index d38edfa59..55ce8b521 100644
--- a/src/components/CodeBlock/CodeBlock.tsx
+++ b/src/components/CodeBlock/CodeBlock.tsx
@@ -40,12 +40,37 @@ interface CustomRendererProps {
useInlineStyles: boolean;
}
+const ButtonContainer = styled.div<{ $theme?: CodeThemeType }>`
+ position: absolute;
+ display: flex;
+ opacity: 0;
+ transition: opacity 0.15s ease;
+ @media (prefers-reduced-motion: reduce) {
+ transition: none;
+ }
+ ${({ theme, $theme }) => {
+ const themeName = ($theme ?? theme.name) as CodeThemeType;
+ const bg = theme.click.codeblock[`${themeName}Mode`].color.background.default;
+ return `
+ gap: ${theme.sizes[3]};
+ border-radius: ${theme.click.codeblock.radii.all};
+ padding: ${theme.sizes[2]};
+ top: ${theme.click.codeblock.space.y};
+ right: ${theme.click.codeblock.space.x};
+ background: color-mix(in srgb, ${bg} 90%, transparent);
+ `;
+ }}
+`;
+
const CodeBlockContainer = styled.div<{ $theme?: CodeThemeType }>`
width: 100%;
width: -webkit-fill-available;
width: fill-available;
width: stretch;
position: relative;
+ &:hover ${ButtonContainer}, &:focus-within ${ButtonContainer} {
+ opacity: 1;
+ }
${({ theme, $theme }) => {
const themeName = theme.name as CodeThemeType;
@@ -84,16 +109,6 @@ const CodeContent = styled.code`
color: inherit;
`;
-const ButtonContainer = styled.div`
- position: absolute;
- display: flex;
- ${({ theme }) => `
- gap: 0.625rem;
- top: ${theme.click.codeblock.space.y};
- right: ${theme.click.codeblock.space.x};
- `}
-`;
-
export const CodeBlock = ({
children,
language,
@@ -140,13 +155,15 @@ export const CodeBlock = ({
$theme={theme}
{...props}
>
-
+
{showWrapButton && (
)}
@@ -155,6 +172,9 @@ export const CodeBlock = ({
$copied={copied}
$error={errorCopy}
icon={copied ? 'check' : errorCopy ? 'warning' : 'copy'}
+ aria-label={
+ copied ? 'Code copied' : errorCopy ? 'Failed to copy code' : 'Copy code'
+ }
onClick={copyCodeToClipboard}
/>
diff --git a/src/components/IconButton/IconButton.tsx b/src/components/IconButton/IconButton.tsx
index c697422c8..d0c7fc780 100644
--- a/src/components/IconButton/IconButton.tsx
+++ b/src/components/IconButton/IconButton.tsx
@@ -26,7 +26,19 @@ const iconButtonVariants = cva(styles.iconbutton, {
});
export const IconButton = forwardRef(
- ({ type = 'primary', htmlType, icon, size, disabled, className, ...props }, ref) => {
+ (
+ {
+ type = 'primary',
+ htmlType,
+ icon,
+ size,
+ disabled,
+ className,
+ 'aria-label': ariaLabel,
+ ...props
+ },
+ ref
+ ) => {
const iconName = icon ? icon.toString() : 'unknown icon';
return (
@@ -37,7 +49,7 @@ export const IconButton = forwardRef(
disabled={disabled}
ref={ref}
role="button"
- aria-label={iconName}
+ aria-label={ariaLabel ?? iconName}
>