Skip to content

Commit

Permalink
Separate data attrs and class name demos
Browse files Browse the repository at this point in the history
  • Loading branch information
aarongarciah committed Feb 24, 2025
1 parent 13427f7 commit 62e6c27
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;

@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}

&:active {
background-color: var(--color-gray-100);
}

&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}

.odd {
color: var(--color-red);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* eslint-disable react/button-has-type, jsx-a11y/control-has-associated-label */
'use client';
import * as React from 'react';
import { useRender } from '@base-ui-components/react/use-render';
import styles from './index.module.css';

type CounterState = {
odd: boolean;
};

type CounterProps = {
className?: string | ((state: CounterState) => string);
render?: useRender.RenderProp<CounterState>;
};

function Counter(props: CounterProps) {
const { render, className, ...otherProps } = props;
const [count, setCount] = React.useState(0);
const odd = count % 2 === 1;
const state = React.useMemo(() => ({ odd }), [odd]);

const { renderElement } = useRender({
render: render ?? <button />,
state,
className,
props: {
...otherProps,
type: 'button',
children: `Counter: ${count}`,
onClick: () => setCount((prev) => prev + 1),
'aria-label': `Count is ${count}, click to increase`,
},
});

return renderElement();
}

export default function ExampleCounter() {
return (
<Counter
className={(state) =>
state.odd ? `${styles.Button} ${styles.odd}` : styles.Button
}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
'use client';
export { default as CssModules } from './css-modules';
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
.Text {
font-size: 0.875rem;
line-height: 1rem;
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;

&[data-size='small'] {
font-size: 0.75rem;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}

&[data-size='large'] {
font-size: 1.25rem;
&:active {
background-color: var(--color-gray-100);
}

&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}

&[data-odd] {
color: var(--color-red);
}
}
Original file line number Diff line number Diff line change
@@ -1,42 +1,40 @@
/* eslint-disable react/button-has-type, jsx-a11y/control-has-associated-label */
'use client';
import * as React from 'react';
import { useRender } from '@base-ui-components/react/use-render';
import styles from './index.module.css';

type Size = 'small' | 'medium' | 'large';

type TextState = {
size: Size;
type CounterState = {
odd: boolean;
};

type TextProps = {
className?: string | ((state: TextState) => string);
render?: useRender.RenderProp<TextState>;
size?: Size;
children: React.ReactNode;
type CounterProps = {
className?: string;
render?: useRender.RenderProp<CounterState>;
};

function Text(props: TextProps) {
const { className, render, size = 'medium', ...otherProps } = props;

const state = React.useMemo(() => ({ size }), [size]);
function Counter(props: CounterProps) {
const { render, className, ...otherProps } = props;
const [count, setCount] = React.useState(0);
const odd = count % 2 === 1;
const state = React.useMemo(() => ({ odd }), [odd]);

const { renderElement } = useRender({
render: render ?? <p />,
state,
render: render ?? <button />,
className,
props: { ...otherProps, className: styles.Text },
state,
props: {
...otherProps,
type: 'button',
children: `Counter: ${count}`,
onClick: () => setCount((prev) => prev + 1),
'aria-label': `Count is ${count}, click to increase`,
},
});

return renderElement();
}

export default function ExampleText() {
return (
<div>
<Text size="small">Small size</Text>
<Text size="medium">Medium size</Text>
<Text size="large">Large size</Text>
</div>
);
export default function ExampleCounter() {
return <Counter className={styles.Button} />;
}
33 changes: 25 additions & 8 deletions docs/src/app/(public)/(content)/react/utils/use-render/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
The `useRender` hook allows you to support the same features Base UI provides across all components so that you can also have a consistent experience in your custom components:

- A [render](/react/handbook/composition) prop to override the default rendered element (you may find this concept as "slot" in other libraries).
- [data-attributes](https://base-ui.com/react/handbook/styling#data-attributes) that map to the component's state that can be used for styling.
- [data attributes](https://base-ui.com/react/handbook/styling#data-attributes) that map to the component's state which can be used for styling.
- A [callback](/react/handbook/styling#css-classes) on the `className` prop that enables passing CSS classes depending on the component's state.

## API reference
Expand Down Expand Up @@ -36,7 +36,7 @@ The `useRender` hook allows you to support the same features Base UI provides ac
},
stateAttributesMap: {
type: 'StateDataAttributes<State>',
description: 'A mapping of state to style hooks.',
description: 'A mapping of state to data attributes.',
},
}}
/>
Expand All @@ -55,19 +55,36 @@ This is an example of a Text component supporting the `render` prop.

### State-based data attributes

In the following demo, the Text component adds data attributes based on the component state.
When you pass the `state` option, data attributes based on this state are added automatically. In the following example, the counter text color is set to red when it's an odd number by targeting the corresponding data attribute.

<Demo path="./demos/data-attributes" />

You can customize how data attributes are generated by providing the `stateAttributesMap` option.
To customize how data attributes are generated, use the `stateAttributesMap` option.

```jsx
```jsx {4-6} title="Customizing data attributes"
useRender({
render: render ?? <p />,
props,
render: render ?? <button />,
state,
stateAttributesMap: {
size: (value) => ({ [`data-size-${value}`]: '' }),
odd: (value) => (value ? { ['data-parity']: 'odd' } : { ['data-parity']: 'even' }),
},
});
```
To skip a data attribute, return `null` in the corresponding state key.
```jsx {5} title="Skipping data attributes"
useRender({
render: render ?? <button />,
state,
stateAttributesMap: {
odd: () => null,
},
});
```
### State-based class names
You can define a `className` prop that accepts a callback function, enabling dynamic CSS class assignment based on the component's state. The example below achieves the same result as the state-based attributes approach but uses the `className` callback for styling instead of data attributes.
<Demo path="./demos/class-name" />

0 comments on commit 62e6c27

Please sign in to comment.