Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9a1de8f
major: env locking
3rd-Eden Nov 3, 2025
ed3a184
doc: improve docs
3rd-Eden Nov 3, 2025
d1dff30
fix: update packages with removal of internal props
3rd-Eden Nov 3, 2025
c95d854
fix: use env and use-props as part of tests
3rd-Eden Nov 4, 2025
18135e6
fix: corrected assertion now that we properly merge slots
3rd-Eden Nov 4, 2025
2221d3d
fix: clean up references and dead branches
3rd-Eden Nov 4, 2025
1431ebb
test: clean up tests
3rd-Eden Nov 4, 2025
ed49047
major: effectively breaking how data-override worked before
3rd-Eden Nov 4, 2025
8981a44
fix: 90%, im sad about it
3rd-Eden Nov 4, 2025
8757552
lint: linting
3rd-Eden Nov 4, 2025
85a0f6b
Merge branch 'main' into environment-locks
akazemier-godaddy Nov 4, 2025
3d8c25e
chore: merge origin/main into environment-locks
3rd-Eden Nov 10, 2025
03cf185
fix: it should only trigger on locked
3rd-Eden Nov 10, 2025
886759c
fix: update env
3rd-Eden Nov 10, 2025
2f43a67
fix: data-override should be disabled by default
3rd-Eden Nov 14, 2025
c874953
fix: update tests as data-override is no longer triggering by default
3rd-Eden Nov 14, 2025
fbce5ce
Merge branch 'main' into environment-locks
akazemier-godaddy Nov 14, 2025
46cf74d
Merge branch 'main' into environment-locks
akazemier-godaddy Nov 18, 2025
3810c4b
chore: merge main into environment-locks
3rd-Eden Nov 27, 2025
558dcda
Merge branch 'environment-locks' of github.com:godaddy/bento into env…
3rd-Eden Nov 27, 2025
cbe76cc
chore: merge main - keep data-override disabled by default
3rd-Eden Dec 18, 2025
09861c4
refactor: convert browser tests to use vitest snapshots
3rd-Eden Dec 19, 2025
5fd7f75
docs: clean up
3rd-Eden Dec 19, 2025
5dd38b6
fix: resolve CI failures
3rd-Eden Dec 19, 2025
457d22b
chore: remove withLock HOC - not needed
3rd-Eden Dec 19, 2025
2054f8c
fix: add coverage tests and fix TypeScript errors
3rd-Eden Dec 19, 2025
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
8 changes: 8 additions & 0 deletions .changeset/gold-things-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@bento/environment": major
"@bento/use-props": major
"@bento/slots": major
"@bento/box": major
Comment on lines +2 to +5

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be minor for now just due to how we are not in a full major release

---

data-override attributes are now opt-in using the Environment component
14 changes: 0 additions & 14 deletions apps/docs/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import { mergeConfig } from 'vite';
import packageJson from '@bento/internal-props/package.json' with { type: 'json' };

const __dirname = dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
Expand Down Expand Up @@ -54,23 +53,10 @@ const config: StorybookConfig = {
},

async viteFinal(config) {
const versionMatch = packageJson.version.match(/^(\d+)\.(\d+)\.(\d+)/);
const semver = versionMatch ? versionMatch.slice(1) : ['0', '0', '0'];

const define = {
major: semver[0],
minor: semver[1],
patch: semver[2]
};

return mergeConfig(config, {
// Set envDir to workspace root for proper monorepo support
envDir: resolve(__dirname, '../../../'),

esbuild: {
define
},

// Exclude @bento packages from optimization so they can be watched and hot-reloaded
optimizeDeps: {
exclude: ['@bento/*']
Expand Down
1 change: 0 additions & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"@bento/heading": "*",
"@bento/icon": "*",
"@bento/illustration": "*",
"@bento/internal-props": "*",
"@bento/listbox": "*",
"@bento/pressable": "*",
"@bento/radio": "*",
Expand Down
7 changes: 1 addition & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 23 additions & 1 deletion packages/box/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ export interface EnvContext<Props> {
*/
sprite: string;

/**
* Indicates if the environment is currently locked.
* When locked, modifications applied after the lock are flagged with data-override.
*/
locked: boolean;

/**
* Current lock generation number.
* Increments each time a new Environment with lock={true} is created.
* Used to determine if a slot was added before or after a lock boundary.
*/
lockGeneration: number;

[key: string]: any;
}

Expand All @@ -44,6 +57,12 @@ export interface SlotsContext {
* Indicator if a `components` override has been applied to the parent or current component.
*/
override: boolean;

/**
* Tracks the lock generation for each slot.
* Maps slot name to the lockGeneration value when the slot was first assigned.
*/
slotGenerations: Record<string, number>;
}

export interface BoxContext<Props> {
Expand Down Expand Up @@ -96,13 +115,16 @@ export function defaults(root?: RootNode): BoxContext<any> {
env: {
components: {},
sprite: '',
locked: false,
lockGeneration: 0,
document: () => getDocument(root),
window: () => getWindow(root)
},
slots: {
override: false,
namespace: [],
assigned: {}
assigned: {},
slotGenerations: {}
}
};
}
Expand Down
46 changes: 41 additions & 5 deletions packages/environment/CONCEPTS.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import * as Stories from './environment.stories.tsx';

import ComponentLevelExample from './examples/component-level.tsx?raw';
import CustomButtonExample from './examples/custom-button.tsx?raw';
import LockNoOverrideExample from './examples/lock-no-override.tsx?raw';
import LockWithOverrideExample from './examples/lock-with-override.tsx?raw';

<Meta of={Stories} name="Concepts" />

# Application level configuration

The `Environment` component is the main entry point for configuring Bento
components in your application. It allows you to set up global configurations,
such as components, and other settings that will be applied throughout your
The `Environment` component is the main entry point for configuring Bento
components in your application. It allows you to set up global configurations,
such as components, and other settings that will be applied throughout your
application. This allows you to easily customize the behavior of specific set
of components.

Expand All @@ -25,7 +27,7 @@ Bento. This is useful when you want to customize the behavior or appearance of a
specific component. You can pass a mapping of component names to their
corresponding components. The keys in the object should match the component
names used by Bento, and the values should be the new components you want to
use.
use.

This is useful when you want to use a custom implementation of a component
or when you want to use a different library altogether. For example, if you want
Expand All @@ -34,7 +36,7 @@ it like this:

<Source language='tsx' code={CustomButtonExample} />

This will replace the default Bento button with your custom button component
This will replace the default Bento button with your custom button component
throughout your application.

### Overrides
Expand Down Expand Up @@ -65,3 +67,37 @@ application code. This is useful when you want to test a new component or a new
version of a component without modifying the application code. The custom button example shows how to use a different component implementation for A/B testing:

<Source language='tsx' code={CustomButtonExample} />

## Lock Boundaries

When you're building a design system on top of Bento, you'll often compose components together internally using slots, className, and style props. Without lock boundaries, all these internal modifications get flagged with `data-override` attributes, which makes it really hard to tell the difference between your design system's internal structure and actual customizations made by people using your design system.

Lock boundaries solve this by creating a clear line between "this is how we built it" and "this is what someone changed." Think of it as drawing a circle around your component and saying "everything inside this circle is intentional design system composition."

### How it works

The mechanism uses a generation counter. When you wrap a component with `<Environment lock={true}>`, Bento increments a generation number. Every slot modification gets tagged with the current generation number. Later, when the component renders, Bento checks: was this slot created before or after the lock? If it was created before (lower generation number), it came from a consumer, so it gets flagged. If it was created after (same or higher generation), it's part of your design system's internal structure, so no flag.

Here's a practical example. Let's say you're building a design system and you create a component that internally uses a Button and RadioGroup:

<Source language='tsx' code={LockNoOverrideExample} />

Notice the `<Environment lock={true}>` wrapper. Everything you do with slots inside that wrapper is considered internal composition. The `root.trigger` slot that changes the button text, the `group.label` and `group.description` slots that style the radio group—none of these get `data-override` attributes because they're all happening after the lock.

Now when someone uses your design system component and wants to customize it:

<Source language='tsx' code={LockWithOverrideExample} />

This slot modification happens *before* entering your locked environment (because it's passed as a prop to `DesignSystemVersion`). Bento sees it crossed the lock boundary and adds `data-override="slot"`. Now you know exactly what your consumers are customizing.

### When to use this

Use lock boundaries when you're building a design system that other developers will consume. If you're just building application components or working directly with Bento primitives, you probably don't need this. The lock is specifically for the boundary between design system authors and design system consumers.

You might skip locks if you're debugging and want to see all modifications flagged, or if you're building one-off components that won't be reused across teams.

### What you can do with this information

Once you have accurate `data-override` tracking, you can build tooling around it. You could create a development mode that warns when consumers override certain slots, track which components are being customized most often to inform your roadmap, or generate migration guides when you need to make breaking changes by analyzing which overrides exist in production.

The `data-override` attributes are just data attributes in the DOM. Your dev tools, browser extensions, or analytics can read them to understand how your design system is being used in the wild.
33 changes: 32 additions & 1 deletion packages/environment/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import OverridePropsExample from './examples/override-props.tsx?raw';
import CustomButtonExample from './examples/custom-button.tsx?raw';
import ComponentLevelExample from './examples/component-level.tsx?raw';
import IframeRenderingExample from './examples/iframe-rendering.tsx?raw';
import LockNoOverrideExample from './examples/lock-no-override.tsx?raw';

<Meta of={Stories} name="Overview" />

Expand Down Expand Up @@ -55,7 +56,7 @@ context.

In the example below we are using the `react-frame-component` package to render
our components in an iframe. By passing the new `document` object as `root`
prop, we can ensure that the components that reference the `document` and
prop, we can ensure that the components that reference the `document` and
`window` objects will correctly access the new document and window objects.

<Source language='tsx' code={IframeRenderingExample} />
Expand Down Expand Up @@ -94,3 +95,33 @@ can do it like this:

<Source language='tsx' code={ComponentLevelExample} />
<Story of={Stories.ComponentLevelOverride} />

## Lock Boundaries

If you're building a design system on top of Bento and distributing components to other teams, the `lock` prop helps you distinguish between your internal component composition and modifications made by your consumers.

The problem this solves is simple: when you compose components internally using slots, all those modifications show up with `data-override` attributes. This makes it impossible to tell which modifications are part of your design system's intentional structure versus which ones are customizations made by the people using your components.

Here's what it looks like in practice:

<Source language='tsx' code={LockNoOverrideExample} />

The `<Environment lock={true}>` wrapper tells Bento that everything inside is internal to your design system. When you modify slots within that boundary (like changing the button text or adding className to the radio labels), those modifications won't get flagged with `data-override`.

But when someone using your design system passes their own slots:

```tsx
<DesignSystemComponent
slots={{ 'composed.root.trigger': { children: 'Custom Text' } }}
/>
```

That modification happens outside your lock boundary, so it gets flagged with `data-override="slot"`. Now you can tell the difference.

This information becomes useful when you want to understand how your design system is actually being used. You could build dev tools that warn about certain overrides, track usage patterns to inform your roadmap, or generate migration guides based on which slots people are actually customizing in production.

For more details about how the generation tracking system works and when to use locks, see the [Concepts](/docs/environment-concepts--docs) page.

## Props

<ArgTypes of={Stories.API} />
80 changes: 80 additions & 0 deletions packages/environment/examples/lock-no-override.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Environment } from '@bento/environment';
import { withSlots } from '@bento/slots';
import { useProps } from '@bento/use-props';
import { Button } from '@bento/button';
import { Container } from '@bento/container';
import { RadioGroup, Radio } from '@bento/radio';
/* v8 ignore next */
import React from 'react';

/**
* Interface for component props.
*
* @interface ComponentProps
* @public
*/
interface ComponentProps {
[key: string]: any;
}

/**
* Composite component with internal slots.
*
* @param {ComponentProps} props - The component props.
* @returns {JSX.Element} The rendered composed component.
* @public
*/
const Composed = withSlots('LockNoOverride.Composed', function ComposedComponent(props: ComponentProps) {
const slots = {
'group.label': { className: 'label' },
'group.description': { className: 'describe' }
};

return (
<Container slot="root">
<Button slot="trigger">Press Me</Button>
<RadioGroup id="fruit-group" slots={slots} label="Favorite fruit" description="Pick your favorite">
<Radio value="apple">Apple</Radio>
<Radio value="banana">Banana</Radio>
<Radio value="orange">Orange</Radio>
</RadioGroup>
</Container>
);
});

/**
* Design system component with lock boundary.
*
* @param {ComponentProps} props - The component props.
* @returns {JSX.Element} The rendered design system component.
* @public
*/
export const DesignSystemVersion = withSlots(
'LockNoOverride.DSVersion',
function DSVersionComponent(props: ComponentProps) {
const { props: p } = useProps(props);
const slots = {
'root.trigger': {
children: 'Click Me'
}
};

return (
<Environment lock={true}>
<Composed slot="composed" slots={slots} {...p} />
</Environment>
);
}
);

/**
* Example demonstrating lock with NO consumer overrides.
* Expected: NO data-override attributes anywhere.
* All slots (trigger, label, description) are internal composition.
*
* @returns {JSX.Element} The rendered example.
* @public
*/
export const LockNoOverride: React.FC = function LockNoOverride() {
return <DesignSystemVersion />;
};
16 changes: 16 additions & 0 deletions packages/environment/examples/lock-with-override.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* v8 ignore next */
import React from 'react';
// Reuse the components from the lock-no-override example
import { DesignSystemVersion } from './lock-no-override';

/**
* Example demonstrating lock WITH consumer override.
* Expected: data-override ONLY on the button (trigger slot).
* Consumer modifies the trigger slot, internal label/description are not flagged.
*
* @returns {JSX.Element} The rendered example.
* @public
*/
export const LockWithOverride: React.FC = function LockWithOverride() {
return <DesignSystemVersion slots={{ 'composed.root.trigger': { children: 'Hello World' } }} />;
};
Loading
Loading