diff --git a/CHANGELOG.md b/CHANGELOG.md
index 03de627..84ebe0d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,13 @@ All notable changes to the Ippon UI packages are documented in this file, so con
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), with one entry per release listing the affected package versions.
+## 2026-07-07 — @ippon-ui/styles 0.0.8 · @ippon-ui/react 0.0.7
+
+### Added
+
+- `button` atom and `button-card` organism can be used as links: an `` carrying the same classes renders without underline, and the Pattern Library documentation shows link examples.
+- `IpponLinkButton` and `IpponLinkButtonCard` React components: link (``) counterparts of `IpponButton` and `IpponButtonCard`, sharing their look but exposing only link-relevant props (no `disabled`, loading or popover behavior).
+
## 2026-07-03 — @ippon-ui/styles 0.0.7 · @ippon-ui/react 0.0.6
### Added
diff --git a/react/README.md b/react/README.md
index 7036865..5801651 100644
--- a/react/README.md
+++ b/react/README.md
@@ -37,6 +37,14 @@ export const SaveButton = () => (
);
```
+To navigate to another page instead of triggering a callback, use the link counterparts `IpponLinkButton` and `IpponLinkButtonCard`, which render an `` with the same look:
+
+```tsx
+import { IpponLinkButton } from '@ippon-ui/react';
+
+export const SettingsLink = () => Settings;
+```
+
## Documentation
Every component is documented in [Storybook](https://storybook.js.org). Stories live in
diff --git a/react/package.json b/react/package.json
index 14fc769..e42bdb8 100644
--- a/react/package.json
+++ b/react/package.json
@@ -1,7 +1,7 @@
{
"name": "@ippon-ui/react",
"description": "Ippon UI React Component Library",
- "version": "0.0.6",
+ "version": "0.0.7",
"license": "Apache-2.0",
"repository": {
"type": "git",
@@ -39,7 +39,7 @@
},
"peerDependencies": {
"@ippon-ui/icons": "~0.0.2",
- "@ippon-ui/styles": "~0.0.7",
+ "@ippon-ui/styles": "~0.0.8",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
diff --git a/react/src/Button.tsx b/react/src/Button.tsx
new file mode 100644
index 0000000..58b4612
--- /dev/null
+++ b/react/src/Button.tsx
@@ -0,0 +1,35 @@
+import type { IconClassic, IconLogo, IconVariant } from '@ippon-ui/icons';
+import type { IpponIonProps } from './IpponIon.tsx';
+import { IpponIon } from './IpponIon.tsx';
+import { clsx } from 'clsx';
+
+export type IpponButtonIcon = {
+ name: IconClassic | IconLogo;
+ variant?: IconVariant;
+};
+
+export type IpponButtonColor = 'success' | 'error' | 'information' | 'warning' | 'neutral';
+
+export type IpponButtonVariant = 'secondary' | 'outline' | 'text';
+
+export type IpponButtonSize = 'small' | 'large';
+
+const ButtonIcon = ({ icon, loading }: { icon: IpponButtonIcon; loading?: boolean }) => (
+
+);
+
+export const OptionalButtonIcon = ({
+ icon,
+ loading,
+}: {
+ icon?: IpponButtonIcon;
+ loading?: boolean;
+}) => {
+ if (icon === undefined) {
+ return null;
+ }
+ return ;
+};
diff --git a/react/src/IpponButton.tsx b/react/src/IpponButton.tsx
index 85a8040..c8d3de6 100644
--- a/react/src/IpponButton.tsx
+++ b/react/src/IpponButton.tsx
@@ -1,22 +1,15 @@
import type { DataSelectableWithChildren } from './DataSelectable.ts';
-import type { IconClassic, IconLogo, IconVariant } from '@ippon-ui/icons';
-import type { IpponIonProps } from './IpponIon.tsx';
-import { IpponIon } from './IpponIon.tsx';
+import type {
+ IpponButtonColor,
+ IpponButtonIcon,
+ IpponButtonSize,
+ IpponButtonVariant,
+} from './Button.tsx';
+import { OptionalButtonIcon } from './Button.tsx';
import { optionalToAlternativeClass } from './CAP.ts';
import { clsx } from 'clsx';
import { useState } from 'react';
-type IpponButtonIcon = {
- name: IconClassic | IconLogo;
- variant?: IconVariant;
-};
-
-type IpponButtonColor = 'success' | 'error' | 'information' | 'warning' | 'neutral';
-
-type IpponButtonVariant = 'secondary' | 'outline' | 'text';
-
-type IpponButtonSize = 'small' | 'large';
-
type IpponButtonVanillaProps = {
color?: IpponButtonColor;
variant?: IpponButtonVariant;
@@ -34,20 +27,6 @@ export type IpponButtonProps = DataSelectableWithChildren =>
value !== null && value !== undefined && typeof (value as Promise).then === 'function';
-const ButtonIcon = ({ icon, loading }: { icon: IpponButtonIcon; loading?: boolean }) => (
-
-);
-
-const OptionalButtonIcon = ({ icon, loading }: { icon?: IpponButtonIcon; loading?: boolean }) => {
- if (icon === undefined) {
- return null;
- }
- return ;
-};
-
const LOADING_ICON: IpponButtonIcon = { name: 'sync' };
export const IpponButton = (props: IpponButtonProps) => {
diff --git a/react/src/IpponLinkButton.tsx b/react/src/IpponLinkButton.tsx
new file mode 100644
index 0000000..2fd87c7
--- /dev/null
+++ b/react/src/IpponLinkButton.tsx
@@ -0,0 +1,44 @@
+import type { DataSelectableWithChildren } from './DataSelectable.ts';
+import type {
+ IpponButtonColor,
+ IpponButtonIcon,
+ IpponButtonSize,
+ IpponButtonVariant,
+} from './Button.tsx';
+import { OptionalButtonIcon } from './Button.tsx';
+import { optionalToAlternativeClass } from './CAP.ts';
+import { clsx } from 'clsx';
+
+export type IpponLinkButtonProps = DataSelectableWithChildren<{
+ href: string;
+ color?: IpponButtonColor;
+ variant?: IpponButtonVariant;
+ size?: IpponButtonSize;
+ iconLeft?: IpponButtonIcon;
+ iconRight?: IpponButtonIcon;
+}>;
+
+export const IpponLinkButton = (props: IpponLinkButtonProps) => {
+ const hasIcon = [props.iconLeft, props.iconRight].some((icon) => icon !== undefined);
+
+ return (
+
+
+ {hasIcon && props.children ? (
+ {props.children}
+ ) : (
+ props.children
+ )}
+
+
+ );
+};
diff --git a/react/src/IpponLinkButtonCard.tsx b/react/src/IpponLinkButtonCard.tsx
new file mode 100644
index 0000000..e1be3b8
--- /dev/null
+++ b/react/src/IpponLinkButtonCard.tsx
@@ -0,0 +1,36 @@
+import type { DataSelectableWithChildren } from './DataSelectable.ts';
+import {
+ optionalToAlternativeClass,
+ optionalToPrefixedAlternativeClass,
+ toAlternativeClass,
+} from './CAP.ts';
+import { clsx } from 'clsx';
+import type { IpponCardColor, IpponCardShadowLevel, IpponCardSize } from './Card.ts';
+
+export type IpponLinkButtonCardProps = DataSelectableWithChildren<{
+ href: string;
+ shadow?: IpponCardShadowLevel;
+ border?: boolean;
+ size?: IpponCardSize;
+ color?: IpponCardColor;
+ fullWidth?: boolean;
+}>;
+
+export const IpponLinkButtonCard = (props: IpponLinkButtonCardProps) => (
+
+ {props.children}
+
+);
diff --git a/react/src/index.ts b/react/src/index.ts
index c048a3c..87e0c69 100644
--- a/react/src/index.ts
+++ b/react/src/index.ts
@@ -9,6 +9,8 @@ export { IpponHSpace, IpponHSpaceSlot } from './IpponHSpace.tsx';
export { IpponIcon } from './IpponIcon.tsx';
export { IpponImportFile } from './IpponImportFile.tsx';
export { IpponIon } from './IpponIon.tsx';
+export { IpponLinkButton } from './IpponLinkButton.tsx';
+export { IpponLinkButtonCard } from './IpponLinkButtonCard.tsx';
export { IpponMeter } from './IpponMeter.tsx';
export { IpponProgress } from './IpponProgress.tsx';
export { IpponSeparator } from './IpponSeparator.tsx';
diff --git a/react/stories/IpponLinkButton.stories.tsx b/react/stories/IpponLinkButton.stories.tsx
new file mode 100644
index 0000000..f91532d
--- /dev/null
+++ b/react/stories/IpponLinkButton.stories.tsx
@@ -0,0 +1,58 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { IpponLinkButton } from '../src/IpponLinkButton.tsx';
+
+const meta = {
+ title: 'Atom/LinkButton',
+ component: IpponLinkButton,
+ args: {
+ children: 'Link button',
+ href: '#',
+ },
+ argTypes: {
+ color: {
+ control: 'select',
+ options: ['success', 'error', 'information', 'warning', 'neutral'],
+ },
+ variant: {
+ control: 'select',
+ options: ['secondary', 'outline', 'text'],
+ },
+ size: {
+ control: 'select',
+ options: ['small', 'large'],
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const Variants: Story = {
+ render: (args) => (
+ <>
+
+ Primary
+ {' '}
+
+ Secondary
+ {' '}
+
+ Outline
+ {' '}
+
+ Text
+
+ >
+ ),
+};
+
+export const WithIcons: Story = {
+ args: {
+ color: 'information',
+ iconLeft: { name: 'open' },
+ iconRight: { name: 'chevron-forward' },
+ },
+};
diff --git a/react/stories/IpponLinkButtonCard.stories.tsx b/react/stories/IpponLinkButtonCard.stories.tsx
new file mode 100644
index 0000000..cd7c27e
--- /dev/null
+++ b/react/stories/IpponLinkButtonCard.stories.tsx
@@ -0,0 +1,43 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { IpponLinkButtonCard } from '../src/IpponLinkButtonCard.tsx';
+import { IpponText } from '../src/IpponText.tsx';
+
+const meta = {
+ title: 'Organism/LinkButtonCard',
+ component: IpponLinkButtonCard,
+ args: {
+ children: Card that navigates to a page,
+ href: '#',
+ },
+ argTypes: {
+ children: { control: false },
+ shadow: {
+ control: 'select',
+ options: [undefined, 'l1', 'l2', 'l3', 'l4', 'l5', 'l6'],
+ },
+ size: {
+ control: 'select',
+ options: [undefined, 'small', 'large'],
+ },
+ color: {
+ control: 'select',
+ options: [undefined, 'success', 'error', 'information', 'warning'],
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: { border: true },
+};
+
+export const WithShadow: Story = {
+ args: { shadow: 'l2' },
+};
+
+export const FullWidth: Story = {
+ args: { border: true, fullWidth: true },
+};
diff --git a/react/test/IpponLinkButton.spec.tsx b/react/test/IpponLinkButton.spec.tsx
new file mode 100644
index 0000000..3be9d15
--- /dev/null
+++ b/react/test/IpponLinkButton.spec.tsx
@@ -0,0 +1,109 @@
+import { afterEach, describe, expect, it } from 'vitest';
+import { render, screen, configure, cleanup } from '@testing-library/react';
+import '@testing-library/jest-dom/vitest';
+import { IpponLinkButton } from '../src';
+
+configure({
+ testIdAttribute: 'data-selector',
+});
+
+const getIpponLinkButton = () => screen.getByTestId('ippon-link-button');
+
+describe('IpponLinkButton', () => {
+ afterEach(cleanup);
+
+ it('should render a link with button classes', () => {
+ render(
+
+ Settings
+ ,
+ );
+
+ const link = getIpponLinkButton();
+
+ expect(link.tagName).toBe('A');
+ expect(link).toHaveAttribute('href', '/settings');
+ expect(link).toHaveClass('ippon-button');
+ expect(link).toHaveTextContent('Settings');
+ });
+
+ describe('Variant', () => {
+ it.each(['secondary', 'outline', 'text'] as const)('should be %s', (variant) => {
+ render(
+
+ {variant}
+ ,
+ );
+
+ expect(getIpponLinkButton()).toHaveClass('ippon-button', `-${variant}`);
+ });
+ });
+
+ describe('Color', () => {
+ it.each(['success', 'error', 'information', 'warning', 'neutral'] as const)(
+ 'should be %s',
+ (color) => {
+ render(
+
+ {color}
+ ,
+ );
+
+ expect(getIpponLinkButton()).toHaveClass('ippon-button', `-${color}`);
+ },
+ );
+ });
+
+ describe('Size', () => {
+ it('should not have size class by default', () => {
+ render(
+
+ Default
+ ,
+ );
+
+ expect(getIpponLinkButton()).not.toHaveClass('-small');
+ expect(getIpponLinkButton()).not.toHaveClass('-large');
+ });
+
+ it.each(['small', 'large'] as const)('should be %s', (size) => {
+ render(
+
+ {size}
+ ,
+ );
+
+ expect(getIpponLinkButton()).toHaveClass(`-${size}`);
+ });
+ });
+
+ describe('Icons', () => {
+ it('should wrap children in text part with icons', () => {
+ render(
+
+ With icons
+ ,
+ );
+
+ const link = getIpponLinkButton();
+
+ expect(link.querySelector('.ippon-button--text')).toHaveTextContent('With icons');
+ expect(link.querySelectorAll('.ippon-button--icon')).toHaveLength(2);
+ });
+
+ it('should not have text part without icons', () => {
+ render(
+
+ No icon
+ ,
+ );
+
+ expect(getIpponLinkButton().querySelector('.ippon-button--text')).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/react/test/IpponLinkButtonCard.spec.tsx b/react/test/IpponLinkButtonCard.spec.tsx
new file mode 100644
index 0000000..df2e9f1
--- /dev/null
+++ b/react/test/IpponLinkButtonCard.spec.tsx
@@ -0,0 +1,99 @@
+import { describe, it, expect, afterEach } from 'vitest';
+import { render, screen, configure, cleanup } from '@testing-library/react';
+import '@testing-library/jest-dom/vitest';
+import { IpponLinkButtonCard } from '../src';
+
+configure({
+ testIdAttribute: 'data-selector',
+});
+
+const getIpponLinkButtonCard = () => screen.getByTestId('ippon-link-button-card');
+
+describe('IpponLinkButtonCard', () => {
+ afterEach(cleanup);
+
+ it('should render a link with button card classes', () => {
+ render(
+
+ Content
+ ,
+ );
+
+ const link = getIpponLinkButtonCard();
+
+ expect(link.tagName).toBe('A');
+ expect(link).toHaveAttribute('href', '/details');
+ expect(link).toHaveClass('ippon-button-card');
+ expect(link).toHaveTextContent('Content');
+ });
+
+ describe('Shadow', () => {
+ it.each(['l1', 'l2', 'l3', 'l4', 'l5', 'l6'] as const)('should have shadow %s', (level) => {
+ render(
+
+ Content
+ ,
+ );
+
+ expect(getIpponLinkButtonCard()).toHaveClass(`-shadow-${level}`);
+ });
+ });
+
+ describe('Border', () => {
+ it('should not have -border class by default', () => {
+ render(
+
+ Content
+ ,
+ );
+
+ expect(getIpponLinkButtonCard()).not.toHaveClass('-border');
+ });
+
+ it('should have -border class when border is present', () => {
+ render(
+
+ Content
+ ,
+ );
+
+ expect(getIpponLinkButtonCard()).toHaveClass('-border');
+ });
+ });
+
+ describe('Full width', () => {
+ it('should have -full-width class when fullWidth is present', () => {
+ render(
+
+ Content
+ ,
+ );
+
+ expect(getIpponLinkButtonCard()).toHaveClass('-full-width');
+ });
+ });
+
+ describe('Size', () => {
+ it.each(['small', 'large'] as const)('should be %s', (size) => {
+ render(
+
+ Content
+ ,
+ );
+
+ expect(getIpponLinkButtonCard()).toHaveClass(`-${size}`);
+ });
+ });
+
+ describe('Color', () => {
+ it.each(['success', 'error', 'information', 'warning'] as const)('should be %s', (color) => {
+ render(
+
+ Content
+ ,
+ );
+
+ expect(getIpponLinkButtonCard()).toHaveClass(`-${color}`);
+ });
+ });
+});
diff --git a/styles/package.json b/styles/package.json
index 4c109b9..2f06787 100644
--- a/styles/package.json
+++ b/styles/package.json
@@ -1,6 +1,6 @@
{
"name": "@ippon-ui/styles",
- "version": "0.0.7",
+ "version": "0.0.8",
"description": "Ippon UI Pattern Library",
"repository": {
"type": "git",
diff --git a/styles/src/atom/atom.pug b/styles/src/atom/atom.pug
index 20cc74d..ec36831 100644
--- a/styles/src/atom/atom.pug
+++ b/styles/src/atom/atom.pug
@@ -15,7 +15,7 @@ block content
.tikui-vertical-spacing--line
include:componentDoc(height=620) badge/badge.md
.tikui-vertical-spacing--line
- include:componentDoc(height=680) button/button.md
+ include:componentDoc(height=740) button/button.md
.tikui-vertical-spacing--line
include:componentDoc(height=50) icon/icon.md
.tikui-vertical-spacing--line
diff --git a/styles/src/atom/button/_button.scss b/styles/src/atom/button/_button.scss
index d309766..4ae3c28 100644
--- a/styles/src/atom/button/_button.scss
+++ b/styles/src/atom/button/_button.scss
@@ -52,6 +52,7 @@
background-color: var(--_background-color);
padding: var(--_inner-padding-vertical) var(--_inner-padding-horizontal);
vertical-align: middle;
+ text-decoration: none;
color: var(--_color);
cursor: pointer;
diff --git a/styles/src/atom/button/button.code.pug b/styles/src/atom/button/button.code.pug
index 40648a6..bd43add 100644
--- a/styles/src/atom/button/button.code.pug
+++ b/styles/src/atom/button/button.code.pug
@@ -21,6 +21,12 @@ mixin ippon-button-example-colors
each color in colors
+ippon-button-example-variants({ color })
+mixin ippon-button-example-links
+ - const variants = [null, 'secondary', 'outline', 'text'];
+ each variant in variants
+ +ippon-button({ href: '#', variant: variant })
+ = `Link ${variant || 'primary'}`
+
mixin ippon-button-example-icons(size)
+ippon-button({ size, icon: { name: 'heart' } })
+ippon-button({ size, variant: 'secondary', iconLeft: { name: 'logo-angular' } }) Icon left
@@ -36,3 +42,4 @@ mixin ippon-button-example-icons(size)
+ippon-button-example-icons('small')
+ippon-button-example-icons
+ippon-button-example-icons('large')
++ippon-button-example-links
diff --git a/styles/src/atom/button/button.md b/styles/src/atom/button/button.md
index f3debe2..9964df2 100644
--- a/styles/src/atom/button/button.md
+++ b/styles/src/atom/button/button.md
@@ -21,6 +21,10 @@
- Loading `-loading` (adds `disabled` and `aria-busy="true"`)
- When a right icon is present, replace it with a `sync` icon (the CSS animation is handled by `-loading`)
+**Link:**
+
+Use an `` tag instead of a `