diff --git a/.changeset/feat-data-modules-size.md b/.changeset/feat-data-modules-size.md
new file mode 100644
index 00000000..8b85e79e
--- /dev/null
+++ b/.changeset/feat-data-modules-size.md
@@ -0,0 +1,5 @@
+---
+'@lglab/react-qr-code': minor
+---
+
+Add `size` option to `dataModulesSettings`. Sets a fixed module size multiplier (1 = full size) for the fillable data module styles (square, pinched-square, circle, diamond, heart, star, hashtag). Ignored when `randomSize` is true.
diff --git a/apps/docs/public/llms-full.txt b/apps/docs/public/llms-full.txt
index 666cead6..7f2fbd94 100644
--- a/apps/docs/public/llms-full.txt
+++ b/apps/docs/public/llms-full.txt
@@ -37,6 +37,7 @@ The main component exported by the library.
| `color` | `string` | - | Color of the data modules (overridden by `gradient`). |
| `style` | `DataModulesStyle` | `'square'` | Shape of the data modules. |
| `randomSize` | `boolean` | `false` | If true, modules will have slightly varied sizes. |
+| `size` | `number` | `1` | Fixed module size multiplier (1 = full size). Keep between 0.75 and 1 for best results — lower values may degrade scannability. Only applies to fillable styles (square, pinched-square, circle, diamond, heart, star, hashtag). Ignored when `randomSize` is true. |
**Available Styles (`DataModulesStyle`):**
`'square'`, `'square-sm'`, `'pinched-square'`, `'rounded'`, `'leaf'`, `'vertical-line'`, `'horizontal-line'`, `'circuit-board'`, `'circle'`, `'diamond'`, `'star'`, `'heart'`, `'hashtag'`
diff --git a/apps/docs/src/app/data-modules-settings/page.tsx b/apps/docs/src/app/data-modules-settings/page.tsx
index 66cb122a..cee198e3 100644
--- a/apps/docs/src/app/data-modules-settings/page.tsx
+++ b/apps/docs/src/app/data-modules-settings/page.tsx
@@ -45,13 +45,29 @@ const props: Prop[] = [
description: (
<>
If true, the modules will have random sizes. Can only be used with styles{' '}
- square, circle, diamond, star,{' '}
- heart and hashtag.
+ square, pinched-square, circle,{' '}
+ diamond, star, heart and{' '}
+ hashtag.
>
),
defaultValue: 'false',
possibleValues: ['true', 'false'],
},
+ {
+ name: 'size',
+ type: 'number',
+ description: (
+ <>
+ Fixed size multiplier applied to each data module (1 = full size). Keep between{' '}
+ 0.75 and 1 for best results — lower values may degrade
+ scannability. Only applies to styles square,{' '}
+ pinched-square, circle, diamond,{' '}
+ star, heart and hashtag. Ignored when{' '}
+ randomSize is true.
+ >
+ ),
+ defaultValue: '1',
+ },
]
export default function Page() {
diff --git a/apps/docs/src/components/demo/data-modules.tsx b/apps/docs/src/components/demo/data-modules.tsx
index 0c66a0cf..5281879d 100644
--- a/apps/docs/src/components/demo/data-modules.tsx
+++ b/apps/docs/src/components/demo/data-modules.tsx
@@ -2,6 +2,7 @@ import { type DataModulesStyle, type ReactQRCodeProps } from '@lglab/react-qr-co
import { type Dispatch } from 'react'
import { FormCheckbox, FormField } from '@/components/ui/form-fields'
+import { Slider } from '@/components/ui/slider'
import { Button } from '../ui/button'
@@ -57,6 +58,9 @@ export const DataModules = ({ qrProps, setQrProps }: DataModulesProps) => {
'hashtag',
].includes(qrProps.dataModulesSettings?.style ?? '')
+ const size = qrProps.dataModulesSettings?.size ?? 1
+ const randomSize = qrProps.dataModulesSettings?.randomSize ?? false
+
return (
<>
@@ -76,13 +80,34 @@ export const DataModules = ({ qrProps, setQrProps }: DataModulesProps) => {
{canBeRandomSize && (
-
- onCheckboxChange(checked as boolean, 'randomSize')
- }
- />
+ <>
+
+ onCheckboxChange(checked as boolean, 'randomSize')
+ }
+ />
+
+
+ setQrProps((prevProps) => ({
+ ...prevProps,
+ dataModulesSettings: {
+ ...prevProps.dataModulesSettings,
+ size: value,
+ },
+ }))
+ }
+ min={0.75}
+ max={1}
+ step={0.01}
+ disabled={randomSize}
+ className={randomSize ? 'opacity-50' : undefined}
+ />
+
+ >
)}
>
)
diff --git a/packages/react-qr-code/src/components/data-modules.tsx b/packages/react-qr-code/src/components/data-modules.tsx
index 10b715db..56d9c23b 100644
--- a/packages/react-qr-code/src/components/data-modules.tsx
+++ b/packages/react-qr-code/src/components/data-modules.tsx
@@ -41,7 +41,7 @@ export const DataModules = ({
gradient,
gradientId,
}: DataModulesProps): ReactNode => {
- const { color, style, randomSize } = useMemo(
+ const { color, style, randomSize, size } = useMemo(
() => sanitizeDataModulesSettings(settings),
[settings],
)
@@ -51,8 +51,8 @@ export const DataModules = ({
const isRandom = dataModuleCanBeRandomSize(style) && randomSize
const scaleFactor = useCallback(
- () => getScaleFactor(style, isRandom),
- [style, isRandom],
+ () => getScaleFactor(style, isRandom, size),
+ [style, isRandom, size],
)
modules.forEach((row, y) => {
diff --git a/packages/react-qr-code/src/types/lib.ts b/packages/react-qr-code/src/types/lib.ts
index 389e4660..d2291290 100644
--- a/packages/react-qr-code/src/types/lib.ts
+++ b/packages/react-qr-code/src/types/lib.ts
@@ -47,6 +47,14 @@ export interface DataModulesSettings {
color?: string
style?: DataModulesStyle
randomSize?: boolean
+ /**
+ * Fixed size multiplier applied to each data module (1 = full size). Keep
+ * between 0.75 and 1 for best results — lower values may degrade
+ * scannability. Only applies to fillable styles (square, pinched-square,
+ * circle, diamond, heart, star, hashtag). Ignored when `randomSize` is true.
+ * @defaultValue 1
+ */
+ size?: number
}
export type FinderPatternOuterStyle =
diff --git a/packages/react-qr-code/src/utils/data-modules.test.ts b/packages/react-qr-code/src/utils/data-modules.test.ts
index facb457e..72d9cef5 100644
--- a/packages/react-qr-code/src/utils/data-modules.test.ts
+++ b/packages/react-qr-code/src/utils/data-modules.test.ts
@@ -25,6 +25,24 @@ describe('getScaleFactor', () => {
const scaleFactor = getScaleFactor('circle', true)
expect(scaleFactor).toEqual(0.9 * (1 - 0.75) + 0.75)
})
+
+ it('returns the provided size for fillable styles when randomSize is false', () => {
+ expect(getScaleFactor('circle', false, 0.8)).toBe(0.8)
+ expect(getScaleFactor('square', false, 0.9)).toBe(0.9)
+ })
+
+ it('ignores size for styles that cannot be scaled', () => {
+ expect(getScaleFactor('rounded', false, 0.8)).toBe(1)
+ expect(getScaleFactor('leaf', false, 0.8)).toBe(1)
+ expect(getScaleFactor('vertical-line', false, 0.8)).toBe(1)
+ expect(getScaleFactor('horizontal-line', false, 0.8)).toBe(1)
+ expect(getScaleFactor('circuit-board', false, 0.8)).toBe(1)
+ })
+
+ it('ignores size when randomSize is true', () => {
+ vi.spyOn(Math, 'random').mockReturnValue(0.5)
+ expect(getScaleFactor('circle', true, 0.8)).toEqual(0.5 * (1 - 0.75) + 0.75)
+ })
})
describe('dataModuleCanBeRandomSize', () => {
diff --git a/packages/react-qr-code/src/utils/data-modules.ts b/packages/react-qr-code/src/utils/data-modules.ts
index 1c185608..c3fb1877 100644
--- a/packages/react-qr-code/src/utils/data-modules.ts
+++ b/packages/react-qr-code/src/utils/data-modules.ts
@@ -12,11 +12,13 @@ export const dataModuleCanBeRandomSize = (style: DataModulesStyle): boolean =>
style === 'diamond' ||
style === 'hashtag'
-export const getScaleFactor = (style: string, isRandom: boolean) => {
+export const getScaleFactor = (style: string, isRandom: boolean, size = 1) => {
if (style === 'square-sm') {
return 0.75
} else if (isRandom) {
return Math.random() * (1 - 0.75) + 0.75
+ } else if (dataModuleCanBeRandomSize(style as DataModulesStyle)) {
+ return size
}
return 1
}
diff --git a/packages/react-qr-code/src/utils/settings.ts b/packages/react-qr-code/src/utils/settings.ts
index d52de2bb..84753a03 100644
--- a/packages/react-qr-code/src/utils/settings.ts
+++ b/packages/react-qr-code/src/utils/settings.ts
@@ -15,6 +15,7 @@ export const sanitizeDataModulesSettings = (settings?: DataModulesSettings) => {
color: settings?.color || DEFAULT_DATA_MODULES_COLOR,
style: settings?.style || DEFAULT_DATA_MODULES_STYLE,
randomSize: settings?.randomSize || false,
+ size: settings?.size ?? 1,
}
}